GeekFactory

int128.hatenablog.com

kube2iamをTerraformとHelmfileでデプロイする

Kubernetes workerをEC2インスタンスで実行する場合,何も設定しないとPodはEC2インスタンスのIAMロールを利用します.このままでは,攻撃者が悪意のあるイメージを利用して情報漏洩や破壊を行うリスクがあります.kube2iamを利用すると,Podに適切なIAMロールを割り当てることが可能です.

本稿では,Amazon EKSで以下を利用する方法を説明します.

ここでは例として,S3バケットの読み取りが必要なアプリケーションをPodで実行するケースを考えます.

workerインスタンスへのIAMポリシーの割り当て

kube2iamはAssume Roleという仕組みを利用してPodにIAMロールを割り当てます.Assume Roleに必要なリソースは別のモジュール(kube2iam)で定義します.

# kube2iam/main.tf
variable "worker_iam_role_name" {
  description = "Name of the IAM role of the Kubernetes worker"
}

data "aws_iam_role" "worker" {
  name = var.worker_iam_role_name
}

resource "aws_iam_role_policy" "this" {
  role        = data.aws_iam_role.worker.id
  name_prefix = "kube2iam"
  policy      = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "sts:AssumeRole"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}
EOF
}

data "aws_iam_policy_document" "assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
    principals {
      type        = "AWS"
      identifiers = [data.aws_iam_role.worker.arn]
    }
  }
}

output "assume_role_policy" {
  description = "JSON string of IAM policy for Assume Role"
  value       = data.aws_iam_policy_document.assume_role_policy.json
}

上記のkube2iamモジュールを利用して,Assume Roleを許可するIAMポリシーをworkerインスタンスに割り当てます.terraform-aws-eks module を利用している場合は以下のように定義します.

module "kube2iam" {
  source = "./kube2iam"

  worker_iam_role_name = module.eks.worker_iam_role_name
}

S3アクセスのIAMロールの作成

S3の読み取りアクセスを許可するIAMロールを作成します.ここでは簡単のため,AWSであらかじめ定義されているポリシーを利用します.

resource "aws_iam_role" "s3_readonly" {
  name               = "s3_readonly"
  assume_role_policy = module.kube2iam.assume_role_policy
}

resource "aws_iam_role_policy_attachment" "s3_readonly" {
  role       = aws_iam_role.s3_readonly.id
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}

実際には,IAMロールに利用目的を表す名前を付けた方がよいでしょう.例えば,GitLab Runnerであれば gitlab_runner_build_app だったり,アプリケーションであれば app_frontend といった感じです.

Helm chartのデプロイ

stable/kube2iamをデプロイします.

releases:
  # https://github.com/helm/charts/tree/master/stable/kube2iam
  - name: kube2iam
    namespace: kube-system
    chart: stable/kube2iam
    values:
      - host:
          iptables: true
          # https://github.com/jtblin/kube2iam#iptables
          interface: eni+
        extraArgs:
          # https://github.com/jtblin/kube2iam#base-arn-auto-discovery
          auto-discover-base-arn: ""
        rbac:
          create: true

kube2iamはiptablesを利用してPodの通信を横取りします.amazon-vpc-cniを利用している場合は,上記のようにインタフェース名に eni+ を指定します.Calicoを利用している場合は cali+ になります.詳しくはkube2iamのiptablesセクションを参照してください.

Pod annotationによるIAMロール割り当ての確認

kube2iamを利用すると,Podに iam.amazonaws.com/role アノテーションを付与するとIAMロールが割り当てられるようになります.以下の例ではPodに s3_readonly というIAMロールを割り当てています.

apiVersion: v1
kind: Pod
metadata:
  annotations:
    iam.amazonaws.com/role: s3_readonly
spec:

ここでは,以下のコマンドを実行して動作確認を行います.

kubectl run s3 -i --rm --image fstab/aws-cli --restart=Never --overrides '{"metadata":{"annotations":{"iam.amazonaws.com/role":"s3_readonly"}}}' -- /home/aws/aws/env/bin/aws s3 ls

S3バケットの一覧が表示されれば成功です.

kube2iamが適切に設定されていない場合,PodはworkerインスタンスのIAMロールを利用してしまうため,以下のエラーがでます.

An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied

アノテーションに指定したIAMロールが存在しない場合,以下のエラーが表示されます.

Unable to locate credentials. You can configure credentials by running "aws configure".

アノテーションを指定しない場合は何も割り当てられません.

最後に,動作確認用に作成した以下のIAMロールを削除しておきましょう.

resource "aws_iam_role" "s3_readonly" {
  name               = "s3_readonly"
  assume_role_policy = module.kube2iam.assume_role_policy
}

resource "aws_iam_role_policy_attachment" "s3_readonly" {
  role       = aws_iam_role.s3_readonly.id
  policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}

TerraformでNATインスタンスを管理する

個人のAWS環境でプライベートサブネット構成を検証したいけどNATゲートウェイに毎月3,500円も払えない*1ので,NATインスタンスのTerraformモジュール int128/nat-instance/aws を作りました.主な特徴はこちらです.

  • Auto Scaling GroupによるAuto Healingに対応.インスタンスが落ちても自動復旧します.
  • スポットインスタンスで低コストを実現.t3a.nanoなら月100円程度で運用できます.
  • ENIの付け替えによる固定ソースIPを実現.

使い方

VPCとサブネットを作成するには terraform-aws-modules/vpc/aws モジュールが便利です.VPCとサブネットに加えてNATインスタンスを作成するには下記のように定義します.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"

  name                 = "main"
  cidr                 = "172.18.0.0/16"
  azs                  = ["us-west-2a", "us-west-2b", "us-west-2c"]
  private_subnets      = ["172.18.64.0/20", "172.18.80.0/20", "172.18.96.0/20"]
  public_subnets       = ["172.18.128.0/20", "172.18.144.0/20", "172.18.160.0/20"]
  enable_dns_hostnames = true
}

module "nat" {
  source = "int128/nat-instance/aws"

  name                        = "main"
  vpc_id                      = module.vpc.vpc_id
  public_subnet               = module.vpc.public_subnets[0]
  private_subnets_cidr_blocks = module.vpc.private_subnets_cidr_blocks
  private_route_table_ids     = module.vpc.private_route_table_ids
}

Terraformを実行すると,下図のようなリソースが作成されます.(プライベートサブネットの各EC2インスタンスは除く)

https://raw.githubusercontent.com/int128/terraform-aws-nat-instance/v0.4.1/diagram.svg?sanitize=true

動作確認のため,AWSコンソールでプライベートサブネットにEC2インスタンスを作成しましょう.作成したEC2インスタンスSSMセッションマネージャでログインして,外部にアクセスできるか確認してみましょう.

仕組み

Terraformを実行すると,Auto Scaling Groupとそれに必要なリソース群(セキュリティグループ,ENI,EIPなど)が作成されます.また,プライベートサブネットのデフォルトルートがENIに設定されます.これにより,プライベートサブネットから外部への通信はENIを通るようになります.

Auto Scaling GroupがNATインスタンスを起動すると,User Dataにある以下のスクリプトが実行されます.

  1. あらかじめ確保しておいたENIをeth1にアタッチする.
  2. IP forwardingのカーネルパラメータを有効にする.
  3. iptablesでIP masqueradeを有効にする.
  4. デフォルトゲートウェイをeth1に変更する.

これにより,プライベートサブネット内のEC2インスタンスはENIとNATインスタンスを経由して通信できるようになります.

もしNATインスタンスが停止した場合は,Auto Scaling Groupによって新しいNATインスタンスが作成されます.既存のENIとEIPをアタッチするため,外部通信のソースIPを固定できます.

User DataでENIのアタッチとデフォルトゲートウェイを変更する部分は試行錯誤が必要でかなり苦労しました.

おまけ

マネージドサービスとは逆行する使い方になりますが,iptablesでDNATを有効にするとNATインスタンスのポートをプライベートサブネットに転送できます.例えば,以下のようなスクリプトを追加すれば,NATインスタンスの443ポートをプライベートサブネットにあるEC2インスタンスに転送できます.NLBに課金する代わりにどうぞ.

# Look up the target instance
tag_name="TARGET_TAG"
target_private_ip="$(aws ec2 describe-instances --filters "Name=tag:Name,Values=$tag_name" | jq -r .Reservations[0].Instances[0].PrivateIpAddress)"

# Expose the port of the NAT instance.
iptables -t nat -A PREROUTING -m tcp -p tcp --dst "${eni_private_ip}" --dport 8080 -j DNAT --to-destination "$target_private_ip:8080"

Terraformモジュールの詳細については下記を参照してください.

github.com

*1:NATゲートウェイの代わりにクラフトビールに課金したい

Argo CDでGitLab SSOを利用する

Argo CDは自前でユーザ管理の仕組みを持たず*1,外部のIdentity Providerに認証を移譲するという設計思想になっています.Argo CDのHelm chartにはDexがバンドルされており,様々なIdentity Providerと連携させることが可能です.本稿では,Argo CDでGitLab SSOを利用する方法を紹介します.

Argo CDはOpenID Connectに対応しています.また,GitLabは自身がOpenID Connect Identity Providerになることが可能です.しかし,Argo CDが必要とする groups claimをGitLabがサポートしていないため,直接連携させることはできません.Argo CDのoidc configでGitLabを指定すると,GitLabで以下のエラーが表示されます(argo-cd#1195).

The requested scope is invalid, unknown, or malformed

代わりに,DexのGitLab connectorを利用することで,Argo CD→Dex→GitLabの流れで連携が可能です.

GitLabの設定

admin area→Applicationsもしくはユーザの設定→Applicationsから,新しいアプリケーションを作成します.以下のように設定します.

  • Name: 任意
  • Redirect URI: https://argocd.example.com/api/dex/callback
  • Scopes: read_user, openid

Dex GitLab connectorではOAuth2を利用するとの記述がありますが,実際はOpenID Connectが利用されるようです.openidスコープを許可しないとエラーになります.

Argo CDの設定

Helmfileを利用する場合は以下のように設定します.

repositories:
  - name: argo
    url: https://argoproj.github.io/argo-helm

releases:
  - name: argocd
    namespace: argocd
    chart: argo/argo-cd
    values:
      - config:
          url: https://argocd.example.com
          dexConfig:
            connectors:
              # GitLab SSO
              - type: gitlab
                id: gitlab
                name: GitLab
                config:
                  baseURL: https://gitlab.com
                  clientID: xxxxxxxx
                  clientSecret: xxxxxxxx
        rbac:
          # assign the admin role to everyone
          policyDefault: role:admin

上記ではすべてのユーザにadminロールを割り当てていますが,GitLabグループに応じてアクセス制御を行うとよいでしょう.

動作確認

下図のように LOGIN VIA GITLAB ボタンが表示されるはずです.ボタンをクリックし,GitLabの同意画面が表示された後,ログインできれば成功です.

f:id:int128:20191003133428p:plain

See Also

*1:ローカルでは組み込みのadminユーザのみ利用できます.