GeekFactory

int128.hatenablog.com

Envoy OAuth2 Filterを試す(未完)

EnvoyのOAuth2 Filterを試してみました。残念ながら期待通りに動きませんでした。メモだけ残しておきます。

www.envoyproxy.io

構成

  • EKS 1.18
  • Envoy 1.17.0-dev-483dd3

以下のトラフィックパスを構築します。

Browser
↓
Internet-facing ALB
↓
Service/NodePort
↓
Pod (envoy)
↓
Service
↓
Pod (ingress-nginx)
↓
Ingress (echoserver)
↓
Service (echoserver)
↓
Pod (echoserver)

Route53やALBを設定してインターネットからドメイン名でアクセス可能にしておきます。

また、Google Identity PlatformであらかじめClient IDを作成しておきます。Redirect URIsには https://echoserver.example.com/callback を指定しておきます(example.com は自分のドメインに置き換えてください)。

Envoyの設定

Example configuration を参考にYAMLを書きます。まずは検証のために必要最小限に設定にします。分かりにくいのでコメントを入れました。

# /etc/envoy/envoy.yaml
static_resources:
  listeners:
    - name: listener_0
      address:
        socket_address: { address: 0.0.0.0, port_value: 10000 }
      filter_chains:
        - filters:
            - name: envoy.filters.network.http_connection_manager
              typed_config:
                "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                codec_type: "AUTO"
                stat_prefix: ingress_http
                route_config:
                  virtual_hosts:
                    # すべてのトラフィックを service cluster に転送する
                    - name: service
                      domains: ["*"]
                      routes:
                        - match:
                            prefix: "/"
                          route:
                            cluster: service
                            timeout: 5s
                http_filters:
                  # OAuth2 Filterを適用する
                  - name: envoy.filters.http.oauth2
                    typed_config:
                      "@type": type.googleapis.com/envoy.extensions.filters.http.oauth2.v3alpha.OAuth2
                      config:
                        # Google OAuth2 Endpointを定義する
                        token_endpoint:
                          cluster: auth
                          uri: oauth2.googleapis.com/token
                          timeout: 3s
                        authorization_endpoint: https://accounts.google.com/o/oauth2/v2/auth
                        redirect_uri: "https://%REQ(:authority)%/callback"
                        redirect_path_matcher:
                          path:
                            exact: /callback
                        signout_path:
                          path:
                            exact: /signout
                        credentials:
                          # Client ID/Secretを定義する
                          client_id: REDACTED.apps.googleusercontent.com
                          token_secret:
                            name: token
                            sds_config:
                              path: "/etc/envoy/secret.yaml"
                          hmac_secret:
                            name: hmac
                            sds_config:
                              path: "/etc/envoy/secret.yaml"
                  - name: envoy.router
  clusters:
    - name: service
      connect_timeout: 5s
      type: LOGICAL_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: service
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      # 後続のバックエンドに転送する
                      address: ingress-nginx-controller.ingress-nginx.svc.cluster.local
                      port_value: 80
    - name: auth
      connect_timeout: 5s
      type: LOGICAL_DNS
      lb_policy: ROUND_ROBIN
      load_assignment:
        cluster_name: auth
        endpoints:
          - lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      address: oauth2.googleapis.com
                      port_value: 443
# /etc/envoy/secret.yaml
resources:
  - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret"
    name: token
    generic_secret:
      secret:
        inline_string: REDACTED
  - "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.Secret"
    name: hmac
    generic_secret:
      secret:
        inline_string: REDACTED

Envoyのデプロイ

以下のYAMLをデプロイします。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: envoy
spec:
  replicas: 1
  selector:
    matchLabels:
      app: envoy
  template:
    metadata:
      labels:
        app: envoy
    spec:
      containers:
        - args:
            - --config-path
            - /etc/envoy/envoy.yaml
            - --bootstrap-version
            - "3"
            - --service-node
            - ingress-envoy
            - --service-cluster
            - ingress-envoy
          image: envoyproxy/envoy-dev:483dd3007f15e47deed0a29d945ff776abb37815
          name: envoy
          ports:
            - name: http
              containerPort: 10000
              protocol: TCP
          resources:
            limits:
              memory: 256Mi
            requests:
              cpu: 10m
              memory: 256Mi
          volumeMounts:
            - mountPath: /etc/envoy
              name: envoy
      volumes:
        - name: envoy
          configMap:
            name: envoy
---
apiVersion: v1
kind: Service
metadata:
  name: envoy
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      nodePort: 30280
      protocol: TCP
      targetPort: http
  selector:
    app: envoy

Envoy v3 APIを利用するには引数に --bootstrap-version 3 が必要です。OAuth2 Filter APIはv3で定義されています。

また、 generic_secret を定義するには引数に --service-node --service-cluster が必要です。これらを指定しないと以下のエラーが出ます。

GenericSecretSdsApi: node 'id' and 'cluster' are required. Set it either in 'node' config or via --service-node and --service-cluster options.

動作確認

ブラウザで https://echoserver.example.com にアクセスします。OAuth2 authorization endpointへのリダイレクトまでは動作したのですが、残念ながら以下のエラー画面になってしまいました。

承認エラー エラー 400: invalid_scope Some requested scopes were invalid. {invalid=[user]}

ブラウザのログを確認すると、以下のようなリクエストが飛んでいました。

https://echoserver.example.com
↓302
https://accounts.google.com/o/oauth2/v2/auth?client_id=REDACTED&scope=user&response_type=code&redirect_uri=https%3A%2F%2Fechoserver.example.com%2Fcallback&state=https%3A%2F%2Fechoserver.example.com%2F
↓302
https://accounts.google.com/signin/oauth/error?authError=REDACTED

OAuth2 scopeをemailなどに変更すれば動作すると思いますが、Envoyのドキュメントには設定項目が見当たりませんでした。v1.16時点ではoauth.protoに定義がないようです。ここでお手上げになりました。

(12/25追記) OAuth2 scopeを指定できるようにするPull Requestがあるようです。

github.com

このPull Requestを実際に動かしているコードもあるようです。

github.com

今後の課題

実用になるまでは以下の課題がありそうです。

  • pass_through_matcher でFilterの除外条件を定義できるが実用に耐えられるか。 authority, pathregex matchが使えるので大丈夫そう。
  • email などでリソースにアクセスできる条件を絞り込めるか。現状は UserInfo から属性を取得する実装がなさそうなので無理そう。

EKS Managed Node GroupでSpot Instancesを使う

Amazon EKSのManaged Node Groupsがスポットインスタンスに対応しました。

aws.amazon.com

これまでManaged Node Groupsではオンデマンドしか利用できず、スポットを利用するには自分でAuto Scaling Groups(mixed instance types)を管理する必要がありました。今回のアップデートで簡単にスポットが利用できるようになりました。

設計で気にする点

元記事から重要と思われる点を挙げてみます。

Allocation strategy

まず、スポットインスタンスはなるべく複数のAZやインスタンスタイプに分散して配置されます。ASGでmixed instance typesを利用する場合は lowest-price と capacity-optimized が選べますが、MNGでは capacity-optimized に固定されています。両者の違いは以下です。

具体例は以下の記事が分かりやすいです。

aws.amazon.com

なるべく多くのAZとインスタンスタイプを指定しておくことで、スポット価格が高騰してインスタンスが上がらない確率が低くなります。

Cluster Autoscalerとの併用

Cluster Autoscalerを利用する場合、Cluster Autoscalerが必要台数を正しく計算できるように、MNGには同じCPU数と同じメモリ量のインスタンスタイプを指定する必要があります。これはASGでmixed instance typesを利用する時と同じですね。

docs.aws.amazon.com

Cluster Autoscalerのドキュメントにも記載されています。

github.com

ノード停止時の振る舞い

ASGでスポットを利用する場合はaws-node-termination-handlerを入れる必要がありましたが、MNGでは不要です。もともとMNGではEC2インスタンスの停止時にdrainが実行されますが、スポットの停止通知にも対応したことになります。

github.com

また、ASGのcapacity rebalance機能でなるべく多くのAZやインスタンスタイプをカバーするように動的に再配置されます。 これはスポットの場合のみ有効になるようです。 詳しくは以下の記事が参考になります。

aws.amazon.com

(2023-01-27 追記) ASG の rebalance 機能はスポットに限らずオンデマンドインスタンスでも有効のようです。ASG のイベントに以下が出ている場合は rebalance が原因です。MNG を AZ 単位に分割すると改善が期待できます。

instances were launched to balance instances in zones ap-northeast-1a with other zones resulting in more than desired number of instances in the group.

Launch Templateとの併用

10月のアップデートでMNGでLaunch Templateが利用できるようになりました。Launch Templateでは1種類のインスタンスタイプしか指定できませんが、MNGでは複数のインスタンスタイプを指定できます。スポットを利用する場合は複数のインスタンスタイプを指定することが望ましいので、基本的にMNGで定義することになると思います。以下のドキュメントに記載があります。

docs.aws.amazon.com

Terraformの実装例

Terraform AWS provider v3.19.0からMNGのスポットに対応しています。

ENHANCEMENTS

  • resource/aws_eks_node_group: Add capacity_type argument and support multiple instance_types (Support Spot Node Groups) (#16510)

https://github.com/hashicorp/terraform-provider-aws/releases/tag/v3.19.0

aws_eks_node_group リソースに capacity_type という属性が増えています。デフォルトは ON_DEMAND になっているので、 SPOT を指定します。

resource "aws_eks_node_group" "default" {
  cluster_name    = aws_eks_cluster.example.name
  node_group_name = "default"
  node_role_arn   = aws_iam_role.example.arn
  subnet_ids      = local.private_subnet_ids
  capacity_type   = "SPOT"

  instance_types = [
    # 4 vCPU and 32 GiB
    "r5.xlarge", "r5a.xlarge", "r5n.xlarge",
    "r5d.xlarge", "r5ad.xlarge", "r5dn.xlarge",
  ]
}

なお、EKSクラスタがすでにあってTerraform AWS providerをv3.19.0にバージョンアップすると capacity_type の差分が出てしまう場合があります。私の環境ではEKS 1.15のクラスタで差分が出てしまいました。その場合はignore changesに指定すれば差分が無視されるようになります。

resource "aws_eks_node_group" "default" {
  lifecycle {
    ignore_changes = [
      # 既存クラスタでcapacity_typeの差分が出る場合
      capacity_type,
    ]
  }
}

既存のクラスタcapacity_typeSPOT に切り替える場合は、MNGが再作成されてしまうので注意が必要です。MNGが削除されるタイミングでMNG内の全ノードが停止します。

その他

12月のEKSアップデートでは、Management Consoleにノードやワークロードの一覧が確認できる画面が追加されていました。また、Management Consoleからアドオンをデプロイできるようです。この辺はGKEを意識した機能追加な感じがします。EKSはまだまだ伸び代があって継続的に進化しているので面白いですね。

AWS SSOでサードパーティツールを実行する

AWS SSOを利用すると、IAM Access KeyやIAM Secret Access Keyの代わりにブラウザベースの認証を利用してAWS APIにアクセスできます。一方で、AWS SSOに対応しているものはAWS CLI v2ぐらいしかなく、Terraformなどのサードパーティツールはそのままでは使えません。そのため、AWS SSOでサードパーティツールを利用するためのヘルパーツールがいくつか公開されています。例えば aws2-wrap などがあります。

本稿では、AWS SSOやSTSの仕組みを理解するため、手動でShort-term credentialsを取得してサードパーティツールを実行する方法を紹介します。

AWS SSOはすでに設定済みである前提とします。参考までに、AWS SSOの設定例として公式ブログにある How to use G Suite as an external identity provider for AWS SSO を挙げておきます。

Short-term credentialsの取得

SSOセッションが切れている場合は再ログインします。例えば、G Suiteと連携している場合はブラウザでGoogleのログイン画面が表示されます。

% aws s3 ls

The SSO session associated with this profile has expired or is otherwise invalid. To refresh this SSO session run aws sso login with the corresponding profile.

% aws sso login
Attempting to automatically open the SSO authorization page in your default browser.
If the browser does not open or you wish to use a different device to authorize this request, open the following URL:

https://device.sso.ap-northeast-1.amazonaws.com/
...

まず ~/.aws/config の内容を確認します。ここにはAWS SSOでログインするための設定が格納されています。

[profile example]
sso_start_url = https://d-********.awsapps.com/start
sso_region = ap-northeast-1
sso_account_id = ********
sso_role_name = PowerUserAccess

~/.aws/sso/cache にあるキャッシュファイルの内容を確認します。ここにはSSOログイン時に取得したアクセストークンが格納されています。

{"startUrl": "https://d-********.awsapps.com/start", "region": "ap-northeast-1", "accessToken": "ey********", "expiresAt": "2020-09-09T22:43:10UTC"}

必要な情報が揃ったら get-role-credentials コマンドを実行します。以下の引数が必要です。

  • --role-name: ~/.aws/configsso_role_name を指定します。これはログイン時に選択したロール名になります。
  • --region: ~/.aws/configsso_region を指定します。これはAWS SSOを有効にしたリージョンになります。
  • --account-id: ~/.aws/configsso_account_id を指定します。これはログイン先のAWSアカウントになります。
  • --access-token: ~/.aws/sso/cache にあるキャッシュファイルから accessToken の値を指定します。

get-role-credentials コマンドを実行すると、以下のような出力が得られます。

% aws sso get-role-credentials --role-name PowerUserAccess --region ap-northeast-1 --account-id ******** --access-token "ey********"
{
    "roleCredentials": {
        "accessKeyId": "********",
        "secretAccessKey": "********",
        "sessionToken": "********",
        "expiration": 1599667863000
    }
}

上記で表示されている情報が Short-term credentials になります。

環境変数の設定とコマンドの実行

前項のコマンドで取得した情報を以下の環境変数に設定します。

% export AWS_ACCESS_KEY_ID=********
% export AWS_SECRET_ACCESS_KEY=********
% export AWS_SESSION_TOKEN=********

環境変数を設定した状態でTerraformを実行してみましょう。AWS APIの呼び出しに成功するはずです。

% terraform apply

本稿の内容がAWS SSOやSTSの仕組みを理解する上で助けになれば幸いです。