Terraform – Kubernetes cluster on AWS EC2

Terraform configuration example that creates Kubernetes cluster (Bare Metal) on AWS EC2. Creates Ingress with NodePort. IP addresses Ingress nodes.

This template creates the following EC2 instances:

  • 1 manager
  • 2 workers
  • 2 ingresses

variables.tf

variable "REGION" {
  default = "us-east-1"
}

variable "PROJECT_NAME" {
  default = "artem_k8s"
}

variable "SSH_USER" { 
  default = "ubuntu"
}

variable "SSH_KEY_NAME" { 
  default = "artem.gatchenko"
}

variable "SSH_KEY_PATH" { 
  default = "/home/artem/.ssh/id_rsa"
}

variable "VPC_SUBNET" { 
  default = "192.168.1.0/24"
}

variable "INSTANCE_TYPE" {
  default = "t2.micro"
}

variable "WORKER_NUMBER" {
  default = "2"
}

main.tf

provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias = "vpc"
  region = "${var.REGION}"
}

vpc.tf

// CREATE VPC
resource "aws_vpc" "vpc" {
  cidr_block = "${var.VPC_SUBNET}"
  enable_dns_hostnames = "true"
  enable_dns_support = "true"

  tags {
    Name = "${var.PROJECT_NAME}"
  }
}

// CREATE GATEWAY
resource "aws_internet_gateway" "vpc" {
  vpc_id = "${aws_vpc.vpc.id}"

  tags {
    Name = "${var.PROJECT_NAME}"
  }
}

// CREATE ROUTE TABLE
resource "aws_route_table" "vpc" {
  vpc_id = "${aws_vpc.vpc.id}"
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = "${aws_internet_gateway.vpc.id}"
  }

  tags {
    Name = "${var.PROJECT_NAME}"
  }
}

// CREATE SUBNET
resource "aws_subnet" "vpc" {
  vpc_id     = "${aws_vpc.vpc.id}"
  cidr_block = "${var.VPC_SUBNET}"

  map_public_ip_on_launch = "true"

  tags {
    Name = "${var.PROJECT_NAME}"
  }
}

resource "aws_route_table_association" "vpc" {
  subnet_id      = "${aws_subnet.vpc.id}"
  route_table_id = "${aws_route_table.vpc.id}"
}

// CREATE SECURITY GROUP
resource "aws_security_group" "vpc" {
  vpc_id      = "${aws_vpc.vpc.id}"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow input SSH"
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow input HTTP"
  }

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow input HTTPS"
  }

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    self = true
    description = "Allow triffic between instances in SG"
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
    description = "Allow all ouput traffic"
  }

  tags {
    Name = "${var.PROJECT_NAME}"
    Description = "${var.PROJECT_NAME}"
  }

}

instance_master.tf

// CREATE INSTANCE FOR KUBERNETES MASTER
resource "aws_instance" "master" {
  ami = "ami-0ac019f4fcb7cb7e6"
  instance_type = "${var.INSTANCE_TYPE}"
  key_name      = "${var.SSH_KEY_NAME}"
  vpc_security_group_ids = ["${aws_security_group.vpc.id}"]
  subnet_id = "${aws_subnet.vpc.id}"
  associate_public_ip_address = true
  source_dest_check = false

  connection {
    type     = "ssh"
    user     = "${var.SSH_USER}"
    private_key = "${file("${var.SSH_KEY_PATH}")}"
  }

  provisioner "file" {
    source      = "k8s.sh"
    destination = "/tmp/k8s.sh"
  }

  provisioner "file" {
    source      = "ingress-nginx.yml"
    destination = "/tmp/ingress-nginx.yml"
  }

  provisioner "remote-exec" {
    inline = [
      "sudo chmod +x /tmp/k8s.sh",
      "sudo /tmp/k8s.sh",
      "sudo kubeadm init --apiserver-advertise-address=${aws_instance.master.private_ip} --pod-network-cidr=10.244.0.0/16 --ignore-preflight-errors SystemVerification --ignore-preflight-errors NumCPU",
      "mkdir -p $HOME/.kube",
      "sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config",
      "sudo chown $(id -u):$(id -g) $HOME/.kube/config",
      "echo 'source <(kubectl completion bash)' >> $HOME/.bashrc",
      "kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml"
    ]
  }

  provisioner "local-exec" {
    command = "ssh -o StrictHostKeyChecking=no [email protected]${aws_instance.master.public_ip} -i ${var.SSH_KEY_PATH} 'kubeadm token create --print-join-command' > token"
  }

  provisioner "local-exec" {
    command = "if [ -e ip_ingresses ]; then rm -rf ip_ingresses; fi"
  }

  tags {
    Name = "${var.PROJECT_NAME}_master"
  }

}

instance_workers.tf

// CREATE INSTANCE FOR KUBERNETES WORKERS
resource "aws_instance" "worker" {
  depends_on = ["aws_instance.master"]
  count = "${var.WORKER_NUMBER}"
  ami = "ami-0ac019f4fcb7cb7e6"
  instance_type = "${var.INSTANCE_TYPE}"
  key_name      = "${var.SSH_KEY_NAME}"
  vpc_security_group_ids = ["${aws_security_group.vpc.id}"]
  subnet_id = "${aws_subnet.vpc.id}"
  associate_public_ip_address = true
  source_dest_check = false

  connection {
    type     = "ssh"
    user     = "${var.SSH_USER}"
    private_key = "${file("${var.SSH_KEY_PATH}")}"
  }

  provisioner "file" {
    source      = "k8s.sh"
    destination = "/tmp/k8s.sh"
  }

  provisioner "file" {
    source      = "token"
    destination = "/tmp/token"
  }

  provisioner "remote-exec" {
    inline = [
      "sleep 15",
      "sudo chmod +x /tmp/k8s.sh",
      "sudo /tmp/k8s.sh",
      "sudo `cat /tmp/token` --ignore-preflight-errors=SystemVerification"
    ]
  }

  tags {
    Name = "${var.PROJECT_NAME}_worker"
  }
}

instance_ingresses.tf

// CREATE INSTANCE FOR KUBERNETES WORKERS
resource "aws_instance" "ingress" {
  depends_on = ["aws_instance.master"]
  # depends_on = [
  #   "aws_instance.master",
  #   "aws_instance.ingress",
  # ]
  count = "2"
  ami = "ami-0ac019f4fcb7cb7e6"
  instance_type = "${var.INSTANCE_TYPE}"
  key_name      = "${var.SSH_KEY_NAME}"
  vpc_security_group_ids = ["${aws_security_group.vpc.id}"]
  subnet_id = "${aws_subnet.vpc.id}"
  associate_public_ip_address = true
  source_dest_check = false

  connection {
    type     = "ssh"
    user     = "${var.SSH_USER}"
    private_key = "${file("${var.SSH_KEY_PATH}")}"
  }

  provisioner "local-exec" {
    command = "echo ${self.private_ip} >> ip_ingresses"
  }

  provisioner "file" {
    source      = "k8s.sh"
    destination = "/tmp/k8s.sh"
  }

  provisioner "file" {
    source      = "token"
    destination = "/tmp/token"
  }

  provisioner "remote-exec" {
    inline = [
      "sleep 15",
      "sudo chmod +x /tmp/k8s.sh",
      "sudo /tmp/k8s.sh",
      "sudo `cat /tmp/token` --ignore-preflight-errors=SystemVerification"
    ]
  }

  tags {
    Name = "${var.PROJECT_NAME}_ingress"
  }
}

ingress.tf

resource "null_resource" "ingress" {

  triggers = {
    instance_id = "${aws_instance.master.id}"
  }

  connection {
    type     = "ssh"
    user     = "${var.SSH_USER}"
    host = "${aws_instance.master.public_ip}"
    agent = false
    private_key = "${file("${var.SSH_KEY_PATH}")}"
  }

  provisioner "file" {
    source      = "ip_ingresses"
    destination = "/tmp/ip_ingresses"
  }

  provisioner "remote-exec" {
    inline = [
      "sleep 15",
      "kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml",
      "IP_INGRESS_1=`sed -n 1p /tmp/ip_ingresses`",
      "IP_INGRESS_2=`sed -n 2p /tmp/ip_ingresses`",
      "sed -i \"s/IP_INGRESS_1/$IP_INGRESS_1/g\" /tmp/ingress-nginx.yml",
      "sed -i \"s/IP_INGRESS_2/$IP_INGRESS_2/g\" /tmp/ingress-nginx.yml",
      "INGRESS_NAME_1=ip-$(cat /tmp/ip_ingresses | sed -n 1p | sed 's/\\./-/g')",
      "INGRESS_NAME_2=ip-$(cat /tmp/ip_ingresses | sed -n 2p | sed 's/\\./-/g')",
      "kubectl label node $INGRESS_NAME_1 node-role.kubernetes.io/ingress=ingress",
      "kubectl label node $INGRESS_NAME_2 node-role.kubernetes.io/ingress=ingress",
      "kubectl apply -f /tmp/ingress-nginx.yml"
    ]
  }
  depends_on = [
    "aws_instance.master",
    "aws_instance.ingress",
  ]
}

ingress-nginx.yml

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx
  namespace: ingress-nginx
  labels:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
spec:
  type: NodePort
  ports:
    - name: http
      port: 80
      targetPort: 80
      protocol: TCP
    - name: https
      port: 443
      targetPort: 443
      protocol: TCP
  externalIPs:
    - IP_INGRESS_1
    - IP_INGRESS_2
  selector:
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx

k8s.sh

#!/bin/bash

# THIS SCRIPT FOR INSTALL K8S. SCRIPT FOR UBUNTU

# INSTALL DOCKER
apt update
apt install -y apt-transport-https ca-certificates curl software-properties-common

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -

add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

apt update
apt install -y docker-ce

systemctl enable docker
systemctl start docker

# INSTALL KUBERNETES

curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
 
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
EOF
 
apt update
apt install -y kubelet kubeadm kubectl

systemctl enable kubelet
systemctl start kubelet

output.tf

output "aws-k8s-ingresses-ip" {
  value = "${aws_instance.ingress.*.public_ip}"
}

 

Download all one archive can be here.

 

How to run the Terraform template:

terraform init
terraform plan
terraform apply

Tagged: Tags