AWS Controllers for Kubernetes(ACK) ことはじめ vol.1 ~ 導入編 ~

この記事は AWS Containers Advent Calendar 2021 の24日目の記事です。

クリスマスイブですね!え?もう 2022 年だって!?クリスマスイブはいつでもあなたの心の中に・・・遅くなって本当にすみません。

※ 本記事は、2022/1/10 現在で著者が検証として諸々触ってみた内容をまとめたものです。ACK は現在デベロッパープレビューな OSS です。後述しますが未実装な機能や改善の余地があり、その実装も今後変更される可能性がに大いにございます。

長くなってしまったので、3 編に分けております。この記事は vol.1 です。

目次

はじめに

ACK の話をする前に Kubernetes の話をちょっとだけします。

Kuberberes というソフトウェアが他のコンテナオーケストレーションツールと差別化される一番のポイントは、一貫したデプロイメントモデルを元に、「高い拡張性」を持っていることだと思います。

CNI、CSI、Validating/Mutating Admission Webhook、そして自身でカスタムしたスケジューラーを用意して使うこともできます。これらは ECS など他のコンテナオーケストレーションツールでは持ち合わせない特性です。

そんな Kubernetes の拡張性の中でもおそらく最も有名な拡張が CustomResourceDefinition というKubernetes にオリジナルのリソース定義を追加できる機能です。カスタムリソースそのものは、いわば ConfigMap のようなものでビジネスロジックはもっていません。カスタムリソースの宣言と、リソースの実体との状態差を収束させるビジネスロジックをコントローラーに実装するのが一般的です。これがカスタムコントローラーです。

このような形で、 Kubernetes で自身のオリジナルのリソースや Kubernetes の API を拡張し、カスタムコントローラーを使って、Kubernetes の一貫したデプロイメントモデルによる管理を実現することができます。

AWS Controllers for Kubernetes (ACK) とは

本題です。AWS Controllers for Kubernetes (ACK) は、前述したカスタムリソースやカスタムコントローラーといった仕組みを使って、Kubernetes から直接 S3 バケットや EC2 インスタンス、SNS トピックといった AWS のサービスリソースを定義して使用できるようにする拡張です。

例えば、S3 バケットでいうと以下のようなマニフェスト YAML で定義できます。

apiVersion: s3.services.k8s.aws/v1alpha1
kind: Bucket
metadata:
  name: ack-s3-bucket-hama
spec:
  name: ack-s3-bucket-hama
  versioning:
    status: Enabled
  website:
    indexDocument:
      suffix: index.html
    errorDocument:
      key: error.html

2020 年 8 月に Github にデベロッパープレビューな OSS として公開されました。2018年に公開された AWS Service Operator for Kubernetes の後継となります。AWS の Kubernetes チームによって構築・メンテナンスされています。

CloudFormation や CDK、Terraform など AWS リソースをコード化してデプロイする方法は様々ありますが、ACK もその一つです。

Kubernetes は OSS ではデファクトスタンダードなコンテナオーケストレーションツールと言って過言ないと思いますが、クラウドネイティブなアプリケーションでは、デベロッパーが利用する技術はコンテナだけで完結することはほぼないでしょう。S3、SQS、Lambda、そして DynamoDB などそのつどインフラエンジニアに作成・設定変更を依頼しながら進めるのはなかなかどうして開発スピードが出ません。とはいえ、Kubernetes のマニフェスト定義を覚えたり、デプロイパイプラインを整えるだけでも大変なのに、デベロッパーが新たに AWS インフラの定義方法を覚えたり、デプロイプロセスを定義するのは学習コストもかかります。

そんな時、ACK のようなツールを使うと、Kubernetes という単一のAPIを理解するだけで、コンテナ以外の AWS リソースも使用・管理できます。そして Kubernetes のリソースとして管理できるということは、すでに Kubernetes で活用しているツールを活用できるということです。デプロイメントだと ArgoCDFlux を使った GitOps のようなアプローチを AWS リソースに対してもとれます。

実装としては CDK や eksctl のように CloudFormation のラッパーではないというのも特徴の一つです。ACK のカスタムコントローラーは、AWS SDK for Go を使って各サービスの API を介して直接コントロールします。結果として、CloudFormation を介さない分高速にデプロイできますし、Kubernetes のマニフェストの状態を “single source of truth” にすることができます。

上図は ACK で S3 バケットを作るプロセスです。デプロイフローは以下の通り行われます (簡潔にするため一部省略しています)。

  1. S3 バケットのマニフェスト YAML を作成して、kubectl apply -f s3bucket.yaml でコントロールプレーンの API サーバーにマニフエストを渡す
  2. マニフェストに記載されたリソース情報を etcd に書きこむ
  3. 実行ユーザに作成した旨をレスポンンス
  4. 非同期で、データプレーン上で実行している ack-s3-controller がカスタムリソースの作成を kube-apiserver 経由で検知
  5. ack-s3-controller から S3 Create Bucket の API を実行して、S3 バケットが作成される
  6. kube-apiserver 経由で S3 からレスポンスのあった情報でカスタムリソースのステータスや情報を更新し、宣言された状態に収束する

また、Reconciliation Loop に則って管理されることも期待します。つまり誤って Kubernetes を介さず手動での設定変更や、リソースを削除した時は宣言したマニフェストの状態に収束するようにコントロールし、ガードレール的に機能して欲しいところです。これは CloudFormation や CDK などは持っていない機能です。

vol2 で後述しますが、検証したところ現状 (2022/01/10 現在) は ACK ではマニフェストからの Create/Edit/Delete としては機能しますが、手動での変更に対するマニフェストの状態への収束はされないようでした。

訂正。こちらの Issue に記載の通り、実装としてはあるけど頻繁なチェックはリソースへの負担も大きく現状は8h間隔でのチェックみたいです。

Crossplane では AWS 側にあるリソースの状態監視も行っており、AWS 側を Kubernete に宣言した状態を保つ様に動作するので、ACK でも同様に実装されることを期待したいです。 の方がチェック感覚が短いというところで、どっちがいいのかは微妙なところですね。

Crossplane については vol.3にて触れます。

サポートされているサービスについて

実は、AWS Controllers for Kubernetes はプロジェクト名であって、その名前のリソースは特にありません。AWS サービス毎に一連の CRD/カスタムコントローラーがサービスコントローラーとして用意されています。

ACK サービスコントローラは個別にリリースされ、現状リリースフェーズは様々です。そして、ACK 全体がプレビューなので、ステージとしては RELEASED であってもメンテナンスフェーズとしては、SageMaker 以外現状 PREVIEW です。

2022/9/26 現在の対象サービス

  • RELEASED(GA): SageMaker, AMP, API Gateway, Application Auto Scaling, DynamoDB, ECR, EKS, KMS, Lambda, RDS, S3, Step Functions
  • RELEASED(PREVIEW): ElastiCache, EC2 / VPC, MQ, OpenSearch Servicve, SNS, EMR, IAM, MemoryDB
  • IN PROGRESS: -

最新情報は公式ドキュメントを参照ください。またリリースフェーズの詳細はこちらをどうぞ。

ACK 導入手順

前提条件

ACK は Kubernete 上で動きます。兎にも角にも Kubernetes クラスターがないことには始まりません。本投稿では eksctl で作った EKS クラスターを前提に進めます。AWS を使う前提のソフトウェアですし、特に理由がなければ ACK を実行する Kubernetes クラスターは EKS がいいと思います。

お手元に手軽に遊べる EKS クラスターがない方は、EKS Workshop の Start the workshopLaunch using eksctl を進めると用意できます。

ACK の各サービスコントローラーや CRD は Helm を使ってインストールできます。実行環境に Helm が入っていない方はインストールしておきましょう。ドキュメントによると Helm 3.7 以降が推奨です。

サービスコントローラーインストール

サービスコントローラーの Helm チャートは ECR Public よりダウンロード可能です。

ECR では OCI Artifact をサポートしており、この機能を使って Helm チャートをダウンロードします。Helm 3 における OCI のサポートは 現在 experimental な機能とみなされているため HELM_EXPERIMENTAL_OCI を環境変数に設定する必要があります。

export HELM_EXPERIMENTAL_OCI=1

特定のサービスコントローラーの最新バージョン Helm チャートをダウンロードするために環境変数を設定します。ここでは S3 を例に進めますが、他のサービスで試す場合は、export SERVICE=s3 を別のサービス名にして進めてください。

現在どのサービスの Helm チャートが公開されているかはこちらをご確認ください。

$ export SERVICE=s3
$ export RELEASE_VERSION=`curl -sL https://api.github.com/repos/aws-controllers-k8s/${SERVICE}-controller/releases/latest | grep '"tag_name":' | cut -d'"' -f4`
$ export CHART_EXPORT_PATH=/tmp/chart
$ export CHART_REF=$SERVICE-chart
$ export CHART_REPO=public.ecr.aws/aws-controllers-k8s/$CHART_REF
$ export CHART_PACKAGE=$CHART_REF-$RELEASE_VERSION.tgz

ダウンロード用のディレクトリを作成し、pull して解凍します。

$ mkdir -p $CHART_EXPORT_PATH

$ helm pull oci://$CHART_REPO --version $RELEASE_VERSION -d $CHART_EXPORT_PATH
$ tar xvf $CHART_EXPORT_PATH/$CHART_PACKAGE -C $CHART_EXPORT_PATH

さて準備が整ったので、ack-system という namespace で helm install を実行します。これにより、S3 だと以下のようなリソースがインストールされます。

  • CRD: buckets.s3.services.k8s.aws
  • Deployments: ack-s3-controller
  • ServiceAccount: ack-s3-controller
  • etc(Role, ClusterRole, ClusterRoleBinding…more)
$ export ACK_K8S_NAMESPACE=ack-system
$ export AWS_REGION=<aws region id>

$ helm install --create-namespace --namespace $ACK_K8S_NAMESPACE ack-$SERVICE-controller \
    --set aws.region="$AWS_REGION" \
    $CHART_EXPORT_PATH/$SERVICE-chart

サービスコントローラーのアクセスコントロール

デプロイされたサービスコントローラーから AWS の API を Call するわけなので、IAM がもちろん必要になります。コントローラーには比較的強めのポリシーが必要になることからもノードレベルで IAM Role を付与するのではなく、IAM Roles for Service Accounts (IRSA) で Pod レベルのアクセスコントロールを付与することが推奨されます。

補足ですが、この図のように ACK のコントローラーとアプリケーションを 1 つのクラスターで同居して使うことは Kubernetes の使い方としてもちろん可能です。ただ、個人的には EKS を使うなら ACK 専用のクラスターを立てて運用した方が不便ないかなと思います。

インストールした ack-s3-controller に IRSA を設定していきます。まずは eksctl utils コマンドを使用して、EKS クラスターの OIDC プロバイダーを作成します。eksctl を利用しない場合はこちらの手順を参照ください。

$ export EKS_CLUSTER_NAME=<eks cluster name>
$ export AWS_REGION=<aws region id>
$ eksctl utils associate-iam-oidc-provider --cluster $EKS_CLUSTER_NAME --region $AWS_REGION --approve

アカウント ID や ServiceAccount の名前など、環境変数を設定します。

# 必要に応じてサービス名は書き換えてください
$ SERVICE="s3"

$ AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
$ OIDC_PROVIDER=$(aws eks describe-cluster --name $EKS_CLUSTER_NAME --region $AWS_REGION --query "cluster.identity.oidc.issuer" --output text | sed -e "s/^https:\/\///")
$ ACK_K8S_NAMESPACE=ack-system

$ ACK_K8S_SERVICE_ACCOUNT_NAME=ack-$SERVICE-controller

サービスコントローラー用の IAM Role を作成します。この IAM Role は、クラスターの OIDC プロバイダー、ServiceAccount の Namaspace、および指定の ServiceAccount 名に限定された信頼関係を持つようにしています。

$ read -r -d '' TRUST_RELATIONSHIP <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/${OIDC_PROVIDER}"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "${OIDC_PROVIDER}:sub": "system:serviceaccount:${ACK_K8S_NAMESPACE}:${ACK_K8S_SERVICE_ACCOUNT_NAME}"
        }
      }
    }
  ]
}
EOF
$ echo "${TRUST_RELATIONSHIP}" > trust.json

$ ACK_CONTROLLER_IAM_ROLE="ack-${SERVICE}-controller"
$ ACK_CONTROLLER_IAM_ROLE_DESCRIPTION='IRSA role for ACK ${SERVICE} controller deployment on EKS cluster using Helm charts'
$ aws iam create-role --role-name "${ACK_CONTROLLER_IAM_ROLE}" --assume-role-policy-document file://trust.json --description "${ACK_CONTROLLER_IAM_ROLE_DESCRIPTION}"

$ ACK_CONTROLLER_IAM_ROLE_ARN=$(aws iam get-role --role-name=$ACK_CONTROLLER_IAM_ROLE --query Role.Arn --output text)

サービスコントローラーに付与したい IAM Policy を定義しています。ACK では Github に recommended-policy-arn として各サービスコントローラーに付与する推奨ポリシーが提供されています。S3 だとこちら。ただそこには、FullAccess Policy が書かれています。CRUD 全ての操作を行うコントローラーなので仕方ないと言えばその通りなのですが。もし付与したい操作を制御したい場合はセルフマネージドなポリシーを書いて付与することも可能です。

そして、多くの AWS サービスにはこの FullAccess Policy が用意されている (e.g: AmazonS3FullAccess) のですが、EKS など一部のサービスには FullAccess Policy がないものもあります。その際、ドキュメント上は インラインポリシーで設定しています。

### FullAccess Policy が存在するサービス
$ BASE_URL=https://raw.githubusercontent.com/aws-controllers-k8s/${SERVICE}-controller/main
$ POLICY_ARN_URL=${BASE_URL}/config/iam/recommended-policy-arn
$ POLICY_ARN_STRINGS="$(wget -qO- ${POLICY_ARN_URL})"

### EKS とか FullAccess Policy が存在しないサービス
$ INLINE_POLICY_URL=${BASE_URL}/config/iam/recommended-inline-policy
$ INLINE_POLICY="$(wget -qO- ${INLINE_POLICY_URL})"

そして、先ほど作った IAM Role に IAM Policy をアタッチします。

### FullAccess Policy が存在するサービス
$ while IFS= read -r POLICY_ARN; do
    echo -n "Attaching $POLICY_ARN ... "
    aws iam attach-role-policy \
        --role-name "${ACK_CONTROLLER_IAM_ROLE}" \
        --policy-arn "${POLICY_ARN}"
    echo "ok."
done <<< "$POLICY_ARN_STRINGS"

### EKS とか FullAccess Policy が存在しないサービス
$ if [ ! -z "$INLINE_POLICY" ]; then
    echo -n "Putting inline policy ... "
    aws iam put-role-policy \
        --role-name "${ACK_CONTROLLER_IAM_ROLE}" \
        --policy-name "ack-recommended-policy" \
        --policy-document "$INLINE_POLICY"
    echo "ok."
fi

作成した IAM Role を サービスコントローラーと紐づいている ServiceAccount に Annotation で関連づけます。

$ kubectl describe serviceaccount/$ACK_K8S_SERVICE_ACCOUNT_NAME -n $ACK_K8S_NAMESPACE
$ export IRSA_ROLE_ARN=eks.amazonaws.com/role-arn=$ACK_CONTROLLER_IAM_ROLE_ARN
$ kubectl annotate serviceaccount -n $ACK_K8S_NAMESPACE $ACK_K8S_SERVICE_ACCOUNT_NAME $IRSA_ROLE_ARN

そのままだとサービスコントローラーには関連づいていないので、再起動します。

$ kubectl get deployments -n $ACK_K8S_NAMESPACE
$ kubectl -n $ACK_K8S_NAMESPACE rollout restart deployment <ACK deployment name>

付与する IAM Role や WEB identity token を格納したファイルパスが、サービスコントローラーの環境変数に設定されていることが確認できます。

$ kubectl get pods -n $ACK_K8S_NAMESPACE
$ kubectl describe pod -n $ACK_K8S_NAMESPACE <NAME> | grep "^\s*AWS_"
---
AWS_ROLE_ARN=arn:aws:iam::<AWS_ACCOUNT_ID>:role/<IAM_ROLE_NAME>
AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token

つづく!

さてこれで ACK で AWS リソースを操作する準備が整ったので、色々試していきます。続きます!


関連リンク: