この記事は、Money Forward Engineering Advent Calendar 2021 19日目の投稿です。
こんにちは。 マネーフォワードでエンジニアとして働いているgotoken(@kennygt51)です。 今回は、当社の提供するサービスの一部が稼働している、マルチアカウントAWS環境のEKSクラスタにIRSA(IAM Roles for Service Accounts)を導入した話をします。
IRSAは何を目的としているか?
実際の導入事例について紹介する前に、IRSAとは「何を目的とした機能なのか?」「どのような仕組みなのか?」という点について紹介します。
IRSAとは 「EKSクラスタ上で稼働するPodに対してIAMロールを割り当てる仕組み」 です。
EKS上のPodで稼働するアプリケーションがAWSのリソース(S3やSQSなど)にアクセスする場合、(EC2上で稼働するアプリケーションと同様に)適切なIAMポリシーを付与したIAMロールを割り当てることで、対象のAWSリソースへのアクセスを許可する必要があります。
2019年9月以前は、Pod単位でIAMロールを付与する仕組みがAWS公式でサポートされておらず、Worker NodeのIAMロールにIAMポリシーを付与する必要がありました。このような方式は「Node及びNode上のPodが同じ権限を持ってしまい、最小権限の法則に反する」という課題がありました。(やりたいことを実現するためにはkiamのようなサードパーティのOSSを導入する必要がありました)
そんな中、2019年9月に Pod単位(正確にはService Account単位)でIAMロールを付与する仕組みが公式にサポートされました! それが 「IAM Role for Service Accounts」 略してIRSAと呼ばれている仕組みです。
IRSAはどのような仕組みなのか?
次に、IRSAはどのような仕組みなのか?という点について簡単に説明します。(これが難しい。。)
IRSAを簡潔に説明すると 「KubernetesのServiceAccountをAWSのIAMエンティティと紐付ける仕組み」 といえます。そしてその紐付けは、OpenID Connectの仕組みを通して実現されます。
AWSから見ると、IAMロールをassumeできるトークンに対して、AWSリソースへの認可(≒IAMロールの一時クレデンシャル)を与えます。Kubernetesから見ると、ServiceAccountのProjectedServiceAccountTokenが、IAMロールをassumeするためのトークンという位置づけになります。そのトークンを使ってsts:AssumeRoleWithWebIdentityAPIを呼び出すことで、Kubernetesが発行したトークンをAWSが検証し、IAMロールの一時クレデンシャルと交換します。
これだけだと少し理解するのが難しい気がするので、ここで少し寄り道して、IRSAの仕組みを理解する上で知っておきたい背景知識について解説します。
IAMのWeb Identity Federationとはなにか
sts:AssumeRoleWithWebIdentityAPIについて少し補足します。
当然ではありますが、AWSリソースにアクセスするには認証のためのクレデンシャル(AWSのアクセスキーやシークレットキー)が必要です。例えばモバイルアプリから直接AWSリソースにアクセスしたい時のことを考えてみましょう。モバイルアプリに直接クレデンシャルを埋め込むのはセキュアではないですよね。独自のIDをAWS側で管理するのも大変です。
それを解決するのがWeb Identity Federationです。
OpenID Connectに準拠した外部IdP(Amazon、Facebook、Googleなど)を使ってサインインすることで、外部IdPがトークンを発行します。トークンを受け取ったら、そのトークンをIAMロールにマッピングすることで、AWSリソースへのアクセス権限をもつ一時クレデンシャルを取得し、そのクレデンシャルを用いてAWSリソースにアクセスします。
「①OpenID Connect互換のIdPで認証」した後に、「②IdPが発行するトークン」を元に、「③AWSリソースへの一時的なアクセス権限を発行」する、という流れです。
より詳しく知りたい人はAWSの公式ドキュメントをご覧ください。またWeb Identity Federation Playgroundで実際にsts:AssumeRoleWithWebIdentityを叩くプロセスを体験することができます。
IAM OIDC IdPとはなにか
OIDC準拠の外部IdPとの連携を設定する場合、IAM OIDC IdPを作成して、外部IdPとその設定についてAWSに通知する必要があります。これによって、AWSアカウントとIdPの間の「信頼」が確立され、外部IdPを使ったWeb Identity FederationによるIAMロールのassumeが可能になります。
つまりIAM OIDC IdPは、OpenID Connect準拠の外部IdPとIAMを連携する為のエンティティとしての役割を担います。
なおIRSAのフローにおいては、EKS(Kubernetes)クラスタが外部IdPとして扱われます。
Mutating Admission Webhookとはなにか
KubernetesのAPIを拡張する方法の一つにAdmission Webhook という仕組みがあります。これはKubernetesのリソースに対するリクエストをトリガに、そのAPIリクエストの内容を検証・変更する機能です。
Admission Webhook には「Mutating Admission Webhook(APIリクエストの内容を変更する)」と「Validating Admission Webhook(APIリクエストの内容を検証する)」の2種類が存在します。
具体的な例を挙げるとpod.spec.imagePullPolicyが未指定だとAlwaysがデフォルトでセットされますよね。これはAPIリクエストの内容がAdmission Controllerによって変更されるからです。
Service Account Token Volume Projectionとはなにか
ServiceAccountのTokenを明示的にPodにマウントすることができる仕組みです。詳しくはService Account Token Volume Projectionを参照してください。
改めて、IRSAはどのような仕組みなのか?
以上の前提知識をもとに、IRSAの仕組みを図示してみます。
IRSA以外でWeb Identity Federationの仕組みを使うケース(例えば、モバイルアプリからAWSリソースにアクセスする時に、sts:AssumeRoleWithWebIdentityを使うとき)を図示すると、以下のような流れになります。
IRSAでは、EKSクラスタが外部IdPとして扱われます。EKSクラスタでIAM OIDC identity providerを作成することで、OIDC ProviderとしてのURLが発行されます。OIDC互換のIdPを使ってsts:AssumeRoleWithWebIdentityを呼び出すためには、IAM OIDC IdPを作成して、外部IdP(ここではEKSクラスタ)との信頼関係を確立する必要があります。また前述のWeb Identity Federationのフローの中でsts:AssumeRoleWithWebIdentityAPIを呼び出す時に、外部IdPに認証して取得したトークンをリクエストに含んでいました。IRSAではService Account Token Volume Projectionによってマウントされたトークンがリクエストに含むべきトークンにあたります。
これを図示すると、以下のようになります。
以上が、ざっくりとしたIRSAの仕組みです。
実際の一時クレデンシャルの取得は次のようなフローでおこなわれます。詳細についてはKubernetes サービスアカウントに対するきめ細やかな IAM ロール割り当ての紹介が参考になります。
kubectl apply -f deployment.yamlの実行によってkube-apiserverにリクエストが送られる- ServiceAccountを指定したPodの作成を要求する。
kube-apiserverがリクエストを受け付けた際のadmission controlにおけるMutating Mutating Admission Webhook処理でAmazon EKS Pod Identity Webhookがcallされる。deployment.yamlのspec.template.spec.serviceAccountNameで指定されたService Accountに付与されたannotation(eks.amazonaws.com/role-arn: <iam role arn>)に基づいて、以下の変更をおこなう。- 環境変数(
AWS_ROLE_ARNとAWS_WEB_IDENTITY_TOKEN_FILE)を注入 - ProjectedVolume(
aws-iam-token)を設定
- 環境変数(
- (IRSAに対応する)AWS SDKが
sts:AssumeRoleWithWebIdentityを呼び出して、AWS_ROLE_ARNで指定したIAMロールの一時クレデンシャルを取得する(assume roleする) - 取得した一時クレデンシャルを用いて、AWSのリソースを使用するAPIを呼び出す
以上でIRSAに関する説明は終わりです。ここからようやく本題に入りまして、当社のマルチアカウントAWS環境のEKSクラスタにIRSAを導入した事例を紹介します。
マルチアカウントAWS環境のEKSクラスタに導入するあたって
当社のEKSクラスタにIRSAを導入するにあたっては、次の点を考慮する必要がありました。
- いくつかのサービスが本番環境で稼働している
- 既存のサービスは
kiamを使ってPod単位でのIAMロールの割当をおこなっている
- 既存のサービスは
- マルチアカウントアーキテクチャ対応
- EKSクラスタがあるアカウントとPodに割り当てるIAMロールがあるアカウントが異なる
それぞれの点について詳しく説明していきます。
既にいくつかのサービスが本番環境で稼働している
当社のEKSクラスタはIRSAが登場する前から構築し、IRSAを使っていないサービスが本番環境で稼働していました。なおそれらのサービスのPodにIAMロールを割り当てるためにkiamを導入しています。先程も述べたとおりkiamはIRSA登場以前から存在した、Pod単位でのIAMロールの割当を実現するOSSです。
IRSAとkiamは、assume roleする仕組みが異なります。
kiamはEC2にIAMロールを割り当てた時に一時クレデンシャルを取得するインスタンスメタデータエンドポイント(https://169.254.169.254/latest/meta-data/)へのリクエストを、iptablesで捻じ曲げて、kiam-serverが代わりに一時クレデンシャルを取得する、という仕組みです。
一方でIRSAはメタデータエンドポイントではなくてsts:AssumeRoleWithWebIdentityというAPIを使って一時クレデンシャルを取得します。(そのためIRSAを導入するためにはsts:AssumeRoleWIthWebIdentityAPIを呼び出す新しいAWS SDKを使用する必要があります)
全く異なる仕組みを用いている為、同時に利用することができます。IRSAを利用できる仕組みだけを用意しておき、実運用は徐々に移行、というやり方も可能です。
当社では一旦kiamを使っているサービスはそのままkiamを利用してもらい、新規で稼働するサービスのみIRSAを使ってもらうことにしました。
マルチアカウントアーキテクチャ対応
当社のEKSクラスタが稼働するインフラはマルチテナントEKS&マルチアカウントアーキテクチャを採用しています。これによりEKSクラスタのあるアカウント(クラスタ用アカウント)と、Podに割り当てたいIAMロールのアカウント(サービス別アカウント)が別れているため、EKSクラスタ上のPodには別アカウントで作成されたIAMロールを割り当てたいという要件がありました。
幸いにもIRSAはクロスアカウントでのIAMロールの割当をサポートしているので、異なるアカウントのIAMロールを割り当てることができます。
クロスアカウントでIRSAを使う手順
クロスアカウントでIRSAを使うまでの流れを紹介します。具体的には、次の対応が必要になります。
- EKSクラスタが稼働するアカウントにIAM OIDC IdPを作成する
- 割り当てたいIAMロールが存在するアカウントにIAM OIDC IdPを作成する
- OIDC Provider URLにはAアカウントのEKSのOIDC Provider URLを指定する
図示すると以下のとおりです。
ここから実際の構築の流れを紹介していきます。当社のEKSクラスタはTerraformを用いて構築しているため、Terraformを用いて例を示します。
割り当てたいIAMロールが存在するアカウントにIAM OIDC IdPを作成する
まず初めにIAMロールが存在するアカウントにIAM OIDC IdPを作成します。プロバイダーURLは、クラスタ用アカウントに構築したEKS クラスタの OpenID Connect プロバイダー URL を指定しています。
またthumbprint_listはクラスタ用アカウントに構築したEKSクラスタにおいて、OpenID Connect ID プロバイダーのルート CA サムプリントの取得に従って取得した値です。
もう少し細かく説明します。IAMでOpen ID Connect IdPを作成する場合はthumbprint(暗号化されたハッシュ値)を指定する必要があります。IAMでは「外部のIdPが使用する証明書に署名したルートCA」のサムプリントが必要です。IAMでOIDC IdPを作成する場合、その外部のIdPからのIDを信頼し、AWSアカウントへのアクセス権限を与えることになります。CA証明書のサムプリントを提供することで、CAによって発行された証明書を、登録されているものと同じDNS名で信頼します。これにより、IdPの署名証明書を更新した時に各アカウントの信頼を更新する必要がなくなります。
###################
# for IAM Roles for Service Account
resource "aws_iam_openid_connect_provider" "hoge_irsa" {
client_id_list = ["sts.amazonaws.com"]
# Ref: https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html
thumbprint_list = ["XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"]
url = local.eks_cluster_iam_oidc_provider_url[var.environment]
}
IAM ロールの作成
サービス別アカウント側で、Podに割り当てるIAMロールを作成します。
IAMロールを作成する時には、先程作成したIAM OIDC IdPへの信頼関係を定義します。
principals {
type = "Federated"
identifiers = [
"${aws_iam_openid_connect_provider.eks_cluster.arn}",
]
}
また信頼関係を定義する時にConditionを定義することで、このIAMロールを割り当てることができるServiceAccountを明示的に指定します。これは、想定していないServiceAccountに勝手にIAMロールを割り当てられないようにするためです。 BというシステムのAWSアカウントで作ったIAMロールを、AというシステムのPodに自由に割り当てられるのはよくないですよね。
"Condition": {
"StringEquals": {
"${OIDC_PROVIDER}:sub": "system:serviceaccount:namespace:service-account-name"
}
}
上記を踏まえると、IAMロールを作成するためのコードは、以下のようになります。
data "aws_iam_policy_document" "irsa_s3_echo" {
statement {
effect = "Allow"
principals {
type = "Federated"
identifiers = [
"${aws_iam_openid_connect_provider.irsa.arn}",
]
}
actions = ["sts:AssumeRoleWithWebIdentity"]
condition {
test = "StringEquals"
variable = "${aws_iam_openid_connect_provider.irsa.url}:sub"
values = ["system:serviceaccount:namespace:service-account-name"]
}
}
}
resource "aws_iam_role" "irsa_s3_echo" {
name = "irsa-s3-echo"
assume_role_policy = data.aws_iam_policy_document.irsa_s3_echo.json
}
resource "aws_iam_role_policy_attachment" "eks_log_role_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
role = aws_iam_role.irsa_s3_echo.name
}
(Podに指定する)Service Accountを作成
次にKubernetes側の設定をおこないます。metadata.annotationsに先程作成したassume roleするIAMロールのARNを指定して、ServiceAccountを作成します。
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::XXXXXXXXXXXX:role/s3-echo
name: s3-echo
namespace: default
Deploymentを作成
作成したServiceAccountをPodに付与したDeploymentを作成します。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: awscli
name: awscli
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: awscli
template:
metadata:
labels:
app: awscli
spec:
serviceAccountName: s3-echo
containers:
- image: python:alpine
name: awscli
command:
- sleep
- "1000000"
なおここで作成されるPodには以下のようにaws-iam-tokenがマウントされています。
- mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
name: aws-iam-token
readOnly: true
また次の環境変数が設定されています。
AWS_WEB_IDENTITY_TOKEN_FILE:トークンがマウントされた場所AWS_ROLE_ARN:asume roleするIAMロールのARN
env:
- name: AWS_ROLE_ARN
value: arn:aws:iam::XXXXXXXXXXXX:role/s3-echo
- name: AWS_WEB_IDENTITY_TOKEN_FILE
value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
以上の対応をおこなうことで、作成したIAMロールの一時クレデンシャルを、Podから取得できます。
# aws sts get-caller-identity
{
"UserId": "AROAZWTG4GGEO46A4DLG4:botocore-session-1594975377",
"Account": "XXXXXXXXXXXX",
"Arn": "arn:aws:sts::XXXXXXXXXXXX:assumed-role/s3-echo/botocore-session-1594975377"
}
現在のIRSAの本番運用状況について
IRSAをEKSクラスタに導入した後に構築されたサービスについては、IRSAを使った本番運用がおこなわれています。今のところ大きな問題は発生していません。
また現在も抱えている課題として、IRSA導入前に本番稼働し始めたサービスがkiamを使っており、IRSAとkiamを同時に運用せざるを得ない状況になっています。kiamを使っているサービスをIRSAに移行できないか検討中です。
またIRSAはkiamというOSSを使わないでPod単位でIAMロールを割り当てることができる良い仕組みではあるのですが、利用するにあたっての作業が開発者から見ると若干複雑(IAMロールの信頼関係を設定したりIdPを設定したり)です。もう少しシンプルにできるといいな〜とは思っています。
最後に
マネーフォワードでは、よりよいインフラ、よりよいプラットフォームを作りたいという想いを持ったエンジニアを募集しています。 ご応募お待ちしています。
【サイトのご案内】 ■マネーフォワード採用サイト ■Wantedly ■京都開発拠点
【プロダクトのご紹介】 ■お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android
■ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』
■だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』


