eksctl で VPC を作るのをやめて Terraform で作るようにしました

Anket は EKS を使ってます。
こんな感じ↓
f:id:hatappi1225:20190217113746p:plain

今回やったこと

AWS 内の各サービスは基本は Terraform を使って作成しているのですがEKS周りの必要なものはeksctlを使ってました。

blog.hatappi.me

Anket における Terraform と eksctl の役割をざっと書くとこんな感じ

今回はこの中の eksctl で作成していた VPC を eksctl ではなく Terraform 側で作成するようにしました。

なぜ eksctl で VPC を作成するのをやめたのか

eksctl は本当に便利でコマンド1つたたけば クラスター作ってEC2たててそれをノードとして扱えるようになる。
つまりコマンド実行して待つだけで kubectl が使えるようになる!!
便利!!

AWS の各リソースを1つ1つ eksctl が AWS API をたたいて作っているのではなく AWS CloudFormation を使って作成していて、eksctl create cluster をした時は クラスターの作成、ノードグループの作成の2つのスタックが作成される。

ここで問題になるのが例えば eksctl 側で作成されるスタックの仕様?変更が生じた時を考える。
ノードグループ側の仕様変更が起きた場合は新しいノードグループを eksctl create nodegroup で作って古いほうを削除すれば良いはず。
ただクラスター側に変更があった時に少し厄介だと思っている。
それが↓のissueのようなケース。今回僕も同じようなケースにあたってしまった。

github.com

古い eksctl で作成していたクラスターから 新しい eksctl で nodegroup を追加しようとしたらできなかったやつで、僕のケースの場合は クラスター側のスタックに修正が入っていて eksctl utils update-cluster-stack でもダメだった。
この issue に書いてあるようにクラスターを作りなおすという選択肢をとって単純に新しいクラスターを作りなおした場合に問題になるのが VPC がかわってしまうこと。

EKS だけで構築しているサービスなら問題ないけど Anket のように RDS や ElasticCache のような VPC 内にサービスをおくようなものを使っているとVPCまたぐことになるので不都合が生じてしまう。

一応 eksctl には 既存の VPC を指定できるようなパラメータがあるので↓のような感じで作成はできる。

$ eksctl create cluster \
  --name anket \
  --region ap-northeast-1 \
  --nodes 2 \
  --nodes-min 1 \
  --nodes-max 2 \
  --version=1.11 \
  --vpc-private-subnets=subnet-1111,subnet-2222,subnet-3333 \
  --vpc-public-subnets=subnet-4444,subnet-5555,subnet-6666

とはいえ古いスタックを残しつつ VPC を使い回すのも嫌だったので良い機会だと思って Terraform で VPC を作成してしまおうと思ったわけです。

VPC を Terraform で作成する。

まずは Terraform で VPC を作成します。

locals {
  prefix = "anket"
}

resource "aws_vpc" "main" {
  cidr_block       = "192.168.0.0/16"
  enable_dns_support = true
  enable_dns_hostnames = true

  tags = {
    Name = "${var.vpc_name}"
    Product = "Anket"
  }

  lifecycle {
    ignore_changes = [
      "tags"
    ]
  }
}

# subnet
resource "aws_subnet" "public-subnet" {
  vpc_id                  = "${aws_vpc.main.id}"
  count                   = "${length(var.public_subnets_availability_zones)}"
  availability_zone       = "${element(var.public_subnets_availability_zones, count.index)}"
  cidr_block              = "${element(var.public_subnets_cidr_blocks, count.index)}"

  tags {
    Name = "${local.prefix}-public-${element(var.public_subnets_availability_zones, count.index)}"
    Product = "Anket"
  }

  lifecycle {
    ignore_changes = [
      "tags"
    ]
  }
}

resource "aws_subnet" "private-subnet" {
  vpc_id                  = "${aws_vpc.main.id}"
  count                   = "${length(var.private_subnets_availability_zones)}"
  availability_zone       = "${element(var.private_subnets_availability_zones, count.index)}"
  cidr_block              = "${element(var.private_subnets_cidr_blocks, count.index)}"

  tags {
    Name = "${local.prefix}-private-${element(var.private_subnets_availability_zones, count.index)}"
    Product = "Anket"
  }

  lifecycle {
    ignore_changes = [
      "tags"
    ]
  }
}

# gateway
resource "aws_internet_gateway" "gw" {
  vpc_id = "${aws_vpc.main.id}"

  tags = {
    Name = "${local.prefix}-gw"
    Product = "Anket"
  }
}

resource "aws_eip" "nat" {
  vpc      = true
}

resource "aws_nat_gateway" "nat" {
  allocation_id = "${aws_eip.nat.id}"
  subnet_id     = "${aws_subnet.public-subnet.0.id}"

  depends_on = ["aws_internet_gateway.gw"]

  tags = {
    Name = "${local.prefix}-nat-gw"
    Product = "Anket"
  }
}

# Private Route Table
resource "aws_route_table" "private" {
  vpc_id = "${aws_vpc.main.id}"

  tags = {
    Name = "${local.prefix}-private-rtb"
  }
}

resource "aws_route" "private" {
  route_table_id            = "${aws_route_table.private.id}"
  destination_cidr_block    = "0.0.0.0/0"
  nat_gateway_id  = "${aws_nat_gateway.nat.id}"
}

resource "aws_route_table_association" "private" {
  count                   = "${length(aws_subnet.private-subnet.*.id)}"
  route_table_id = "${aws_route_table.private.id}"
  subnet_id      = "${element(aws_subnet.private-subnet.*.id, count.index)}"
}

# Public Route Table
resource "aws_route_table" "public" {
  vpc_id = "${aws_vpc.main.id}"

  tags = {
    Name = "${local.prefix}-public-rtb"
  }
}

resource "aws_route" "public" {
  route_table_id            = "${aws_route_table.public.id}"
  destination_cidr_block    = "0.0.0.0/0"
  gateway_id  = "${aws_internet_gateway.gw.id}"
}

resource "aws_route_table_association" "public" {
  count                   = "${length(aws_subnet.public-subnet.*.id)}"
  route_table_id = "${aws_route_table.public.id}"
  subnet_id      = "${element(aws_subnet.public-subnet.*.id, count.index)}"
}

基本的に eksctl で作成していたものをトレースしただけですがポイントがあるとしたら vpc や subnet リソースの作成で ignore_changes でタグを指定しているところですかね。

lifecycle {
    ignore_changes = [
      "tags"
    ]
}

eksctl でクラスターを作成した時に vpc や サブネットに自動でタグを付与していきます。
ignore_changes を指定しないと apply した時に差分が出るので eksctl が付与するタグを消してしまうからです。

VPC が作られたら後は subnetを指定して eksctl を作成するだけです。

$ eksctl create cluster \
  --name anket \
  --region ap-northeast-1 \
  --nodes 2 \
  --nodes-min 1 \
  --nodes-max 2 \
  --version=1.11 \
  --vpc-private-subnets=subnet-1111,subnet-2222,subnet-3333 \
  --vpc-public-subnets=subnet-4444,subnet-5555,subnet-6666

最後に

今回 EKS のクラスター 何回も壊して作りなおしたので今後クラスター から作り直すとなってしまった場合でも落ち着いて対応ができそう。(できればやりたくない)