GeekFactory

int128.hatenablog.com

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ユーザのみ利用できます.

AWS Cluster AutoscalerをTerraformとHelmfileでデプロイする

Cluster Autoscalerを利用すると,CPUやメモリの要求量に応じてノード数を自動的に増減させることが可能です.

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

Cluster AutoscalerのPodにIAMロールを割り当てるため,あらかじめkube2iamもしくはkiamをデプロイしておく必要があります.詳しくは下記を参照してください.

int128.hatenablog.com

(2020/9/23 追記)Cluster AutoscalerのHelm chartはgithub.com/kubernetes/autoscalerに移動しました。また、権限の割り当てはIAM Roles for Service Accountsが推奨されています。

Worker Auto Scaling Groupの修正

Auto Scaling GroupにAuto Discovery用のタグを追加します.

terraform-aws-eks module を利用している場合は以下のように autoscaling_enabled = true を追加すればよいです.詳しくはモジュールの Autoscaling を参照してください.

  worker_groups_launch_template_mixed = [
    {
      # 中略
      autoscaling_enabled     = true
      asg_min_size            = 1
      asg_max_size            = 8
      asg_desired_capacity    = 4
    },
  ]

kopsを利用している場合は kops edit ig nodes で以下のようにノード用のタグを追加します.

  cloudLabels:
    k8s.io/cluster-autoscaler/enabled: 1
    k8s.io/cluster-autoscaler/CLUSTER_NAME: 1

IAM Roleの作成

Cluster AutoscalerがAuto Scaling Groupを取得したり変更したりするためのIAM Roleを作成します.必要なIAMポリシーは cluster-autoscaler/cloudprovider/aws に記載されています.

# IAM role for Cluster Autoscaler
resource "aws_iam_role" "cluster_autoscaler" {
  name               = "${var.cluster_name}-cluster-autoscaler"
  assume_role_policy = data.aws_iam_policy_document.kube2iam_assume_role.json
}

resource "aws_iam_role_policy" "cluster_autoscaler" {
  role   = aws_iam_role.cluster_autoscaler.name
  policy = <<EOF
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "autoscaling:DescribeAutoScalingGroups",
                "autoscaling:DescribeAutoScalingInstances",
                "autoscaling:DescribeLaunchConfigurations",
                "autoscaling:DescribeTags",
                "autoscaling:SetDesiredCapacity",
                "autoscaling:TerminateInstanceInAutoScalingGroup"
            ],
            "Resource": "*"
        }
    ]
}
EOF
}

data "aws_iam_policy_document" "kube2iam_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
    principals {
      type        = "AWS"
      identifiers = [module.eks.worker_iam_role_arn]
    }
  }
}

Helm chartのデプロイ

stable/cluster-autoscaler chart をデプロイします.

releases:
  # https://github.com/helm/charts/tree/master/stable/cluster-autoscaler
  - name: cluster-autoscaler
    namespace: kube-system
    chart: stable/cluster-autoscaler
    values:
      - cloudProvider: aws
        awsRegion: ap-northeast-1
        autoDiscovery:
          clusterName: {{ .Environment.Values.clusterName }}
        rbac:
          create: true
        podAnnotations:
          # IAM role for Cluster Autoscaler
          iam.amazonaws.com/role: {{ .Environment.Values.clusterName }}-cluster-autoscaler

  # https://github.com/helm/charts/tree/master/stable/metrics-server
  - name: metrics-server
    namespace: kube-system
    chart: stable/metrics-server

environments:
  default:
    values:
      - clusterName: CLUSTER_NAME

Cluster Autoscalerのログにエラーが出ていないことを確認します.IAMロールやIAMポリシーが不適切な場合はエラーが出ます.

スケールアップの動作確認

ここでは以下のように echoserver をデプロイします.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: echoserver
  namespace: echoserver
spec:
  replicas: 1
  template:
    metadata:
      labels:
        app: echoserver
    spec:
      containers:
      - image: gcr.io/google_containers/echoserver:1.10
        name: echoserver
        ports:
        - containerPort: 8080

ここで,以下のようにCPU requestを設定します.1ノード2コアの場合は1500mにするとよいでしょう.要求値が大きすぎると,Cluster Autoscalerはノードを追加してもスケジュール不可能と判断してしまいます.

spec:
  template:
    spec:
      containers:
      - image: gcr.io/google_containers/echoserver:1.10
        name: echoserver
        ports:
        - containerPort: 8080
           resources:
              requests:
                cpu: 1500m

Cluster Autoscalerのログを確認します.以下のように Upcoming 1 nodes と表示されると成功です.同時に,Auto Scaling GroupのDesired Capacityが書き換わっていることを確認します.しばらく経つと,ノードが増えてPodがスケジュールされるはずです.

I0927 11:50:35.158353       1 scale_up.go:263] Pod echoserver/echoserver-74fd7d865f-vkzqb is unschedulable
I0927 11:50:35.158391       1 scale_up.go:300] Upcoming 0 nodes
I0927 11:50:35.158521       1 scale_up.go:423] Best option to resize: ASG_NAME
I0927 11:50:35.158540       1 scale_up.go:427] Estimated 1 nodes needed in ASG_NAME
I0927 11:50:35.158556       1 scale_up.go:529] Final scale-up plan: [{ASG_NAME 4->5 (max: 8)}]
I0927 11:50:35.158572       1 scale_up.go:694] Scale-up: setting group ASG_NAME size to 5

I0927 11:50:45.523906       1 scale_up.go:263] Pod echoserver/echoserver-74fd7d865f-vkzqb is unschedulable
I0927 11:50:45.524041       1 scale_up.go:300] Upcoming 1 nodes

I0927 11:52:36.144782       1 clusterstate.go:194] Scale up in group ASG_NAME finished successfully in 2m0.794268739s

スケールダウンの動作確認

前のセクションで設定したCPU requestを削除します.

デフォルトでは,Cluster Autoscalerがノードが必要ないと判断してから10分後に実際にノードを削除します(詳しくは How does scale-down work? を参照してください).Cluster Autoscalerのログを確認し,以下のように Terminating EC2 instance が表示されると成功です.

I0927 11:57:07.790306       1 scale_down.go:407] Node ip-172-19-67-52.ap-northeast-1.compute.internal - utilization 0.055000
I0927 11:57:07.790634       1 static_autoscaler.go:359] ip-172-19-67-52.ap-northeast-1.compute.internal is unneeded since 2019-09-27 11:57:07.773690521 +0000 UTC m=+2997.491422805 duration 0s

I0927 12:07:12.161679       1 static_autoscaler.go:359] ip-172-19-67-52.ap-northeast-1.compute.internal is unneeded since 2019-09-27 11:57:07.773690521 +0000 UTC m=+2997.491422805 duration 10m4.367847963s
I0927 12:07:12.161767       1 scale_down.go:600] ip-172-19-67-52.ap-northeast-1.compute.internal was unneeded for 10m4.367847963s
I0927 12:07:12.161868       1 scale_down.go:819] Scale-down: removing empty node ip-172-19-67-52.ap-northeast-1.compute.internal
I0927 12:07:12.176857       1 delete.go:64] Successfully added toBeDeletedTaint on node ip-172-19-67-52.ap-northeast-1.compute.internal
I0927 12:07:12.391908       1 auto_scaling_groups.go:269] Terminating EC2 instance: i-066bc60549f083e38

最後に,Auto Scaling Groupのdesired capacityが元に戻っていることを確認します.