AWS EC2 Instanz erstellen mit Terraform

Simple Anleitung zum erstellen einer EC2 Instanz (Ubuntu 16.04) samt "einfachem" Netzwerk mit Terraform. Für die EC2 kommt zusätzlich noch cloud-init zur Initialisierung zum Einsatz.

Schritte:

  • Netzwerk & Routing & FW-Regeln erstellen
    • Wir erstellen ein VPC mit einem public Subnet und entsprechendem Routing über einen Internet Gateway. Mit Security Gruppen erlauben wir zusätzlich Port 80 und 22 (Egress & Ingress).
  • EC2 Instanz erstellen
    • Initialisieren während dem ersten starten mit "Cloud-Init".
      Cloud-Init führt folgende Schritte durch:
      • Fügt die Paketquelle für "Ansible" hinzu
      • Aktualisiert die vorhandenen Pakete
      • Setzt die Zeitzone auf "Europe/Zurich"
      • Konfiguriert NTP mit Pool/Server
      • Entfernt "snapd"
      • Installiert Pakete: python-pip, nginx, ansible, htop
      • Erstellt einfache "nginx" Standardseite

AWS Netwerkgrundlagen

Das "einfache" Netzwerk ist doch schon ein wenig komplex um es in Terraform abzubilden. Es besteht aus folgenden "Komponenten":

Einige verschiedene Beispiele von Netzwerk Szenarien findest Du hier:
VPC Scenarios

Die verschiedenen Regionen und Zonen von AWS: Regionen und Zonen

Eingesetzte Terraform Version in dieser Anleitung:

$ terraform version                                                                                               
Terraform v0.11.1
+ provider.aws v1.5.0

Legen wir los

Gehe zu deinem Projektordner und Klone das Projekt terraform-aws-example:

git clone https://bitbucket.org/22phuber/terraform-aws-example.git terraform-aws-example
cd terraform-aws-example

AWS Zugriff und SSH Public Key

AWS Access Key & Secret konfigurieren. Dein Public Key einrichten für SSH Zugriff

mv template.terraform.tfvars terraform.tfvars
# Füge deine Zugangsdaten und den SSH public key hinzu
vim terraform.tfvars

AWS_ACCESS_KEY = "enter-your-aws-access"
AWS_SECRET_KEY = "enter-your-aws-secret"
SSH_PUB_KEY    = "enter-your-ssh-public-key"

Installiere Terraform

Terraform herunterladen/installieren:

Terraform Initialisieren

Terraform kann anhand der Konfigurationsdateien erkennen was alles benötigt wird und dementsprechend die Module/Plugins herunterladen. Mehr dazu terraform init

Initialisiere Terraform mit terraform init:

$ terraform init                                                                                                  

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (1.5.0)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 1.5"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Terraform: main.tf

In der Datei main.tf ist die gesamte Terraform-Konfiguration.
https://bitbucket.org/22phuber/terraform-aws-example/src/master/main.tf

Einige Konfigurationsabschnitte schauen wir uns genauer an:

AWS Region und Availability Zone

In diesen zwei Blöcken wird die AWS Provider Informationen konfiguriert:

# Specify the provider and access details
provider "aws" {
  region = "eu-central-1"
  access_key = "${var.AWS_ACCESS_KEY}"
  secret_key = "${var.AWS_SECRET_KEY}"
}

# var availability_zone
variable "desired_availability_zone" {
  default = "eu-central-1a"
}

Mit region = "eu-central-1" wird die AWS Region angegeben.
Ihr könnt diese auch ändern auf eure gewünschte Region. Jedoch muss dann ebenfalls die Availability Zone (default = "eu-central-1a") entsprechend angepasst werden und das AMI = Amazon Machine Image für die EC2 Instanz.
Die Konfiguration für die EC2 ist ganz weit unten in der main.tf:

  # search ami ids => https://cloud-images.ubuntu.com/locator/ec2/
  ami           = "ami-df8406b0" # ami for you region

TIP: Wie im Kommentar ersichtlich, gibt es eine extra URL für Ubuntu cloud EC2 AMIs um die ID zu finden: https://cloud-images.ubuntu.com/locator/ec2/

SSH Key

Dieser Block erstellt ein "SSH key pair" anhand der SSH_PUB_KEY Variable in der terraform.tfvars Datei.

# create a ssh keypair
resource "aws_key_pair" "pubkey" {
  key_name   = "my-pubkey"
  public_key = "${var.SSH_PUB_KEY}"
}

VPC und Subnet

Beim VPC und Subnet kann die CIDR Maske gewählt werden:

# add vpc
resource "aws_vpc" "vpc_terraform" {
  cidr_block = "10.99.0.0/16"
  enable_dns_support = true
  enable_dns_hostnames = true
  tags = {
    Name = "VPC Terraform"
  }
}

# add subnet (public)
resource "aws_subnet" "subnet_public_terraform" {
  vpc_id = "${aws_vpc.vpc_terraform.id}"
  cidr_block ="10.99.1.0/24"
  map_public_ip_on_launch = "true"
  availability_zone = "${var.desired_availability_zone}"
  tags {
    Name = "Subnet (public) Terraform"
  }
}

VPC: cidr_block = "10.99.0.0/16"
Subnet: cidr_block ="10.99.1.0/24"

Security Groups

Mit den folgenden zwei Security Gruppen erlauben wir Port 80 & 22 eingehend und ausgehend:

# create inbound sec group for http
resource "aws_security_group" "sec_group_http80_terraform" {
  name = "Allow HTTP 80"
  tags {
        Name = "Allow HTTP 80"
  }
  description = "Allow HTTP 80 INBOUD"
  vpc_id = "${aws_vpc.vpc_terraform.id}"

  ingress {
    from_port = 80
    to_port = 80
    protocol = "TCP"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  lifecycle {
    create_before_destroy = true
  }
}

# create inbound sec group for ssh
resource "aws_security_group" "sec_group_ssh22_terraform" {
  name = "Allow SSH 22"
  tags {
        Name = "Allow SSH 22"
  }
  description = "Allow SSH 22 INBOUD"
  vpc_id = "${aws_vpc.vpc_terraform.id}"

  ingress {
    from_port   = "22"
    to_port     = "22"
    protocol    = "TCP"
    cidr_blocks = ["0.0.0.0/0"] # optional: you can change this to your public IP/IP-Range
  }
  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  lifecycle {
    create_before_destroy = true
  }
}

Optional kann der CIDR Block für die eingehende SSH Verbindung cidr_blocks = ["0.0.0.0/0"] auch auf die öffentliche IP oder IP-Block eures Netzwerkes geändert werden.

EC2 Instanz

Das erstellen der EC2 Instanz:

# create ec2 instance from ami
resource "aws_instance" "ec2_terraform" {
  # search ami ids => https://cloud-images.ubuntu.com/locator/ec2/
  ami           = "ami-df8406b0" # ami for you region
  instance_type = "t2.micro" # you may choose if you are on free tier: "Free tier eligible"
  subnet_id     = "${aws_subnet.subnet_public_terraform.id}"
  key_name      = "${aws_key_pair.pubkey.key_name}"
  associate_public_ip_address = true
  security_groups = ["${aws_security_group.sec_group_http80_terraform.id}","${aws_security_group.sec_group_ssh22_terraform.id}"]
  lifecycle {
    create_before_destroy = true
  }
  tags {
    Name        = "Terraform-Test"
  }
  # using cloud init => https://cloudinit.readthedocs.io/en/latest/
  user_data     = "${file("user-data.yml")}"
}

instance_type = "t2.micro": Definiere der gewünschten Instanz Typ: https://aws.amazon.com/de/ec2/instance-types/
associate_public_ip_address = true: Mit dieser Option wird eine öffentliche IP erstellt.
Mit lifecycle => create_before_destroy Wir zuerst eine neue EC2 erstellt bevor die "alte" gelöscht wird.
user_data = "${file("user-data.yml")}": Hier wird die cloud-init Konfiguration angeben.

Cloud-Init: user-data.yml

Die cloud-init Konfiguration in yaml Syntax.
https://bitbucket.org/22phuber/terraform-aws-example/src/master/user-data.yml

Hinzufügen der Apt Paketquelle (PPA) für Ansible:

apt:
  sources:
    ansible:
      source: "ppa:ansible/ansible"

Apt aktualisieren, dann upgrade und dem Image erlauben neu zu starten falls nötig mit package_reboot_if_required:

package_update: true
package_upgrade: true
package_reboot_if_required: true

Zeitzone setzen:

timezone: Europe/Zurich

NTP konfigurieren:

ntp:
  pools: [0.ch.pool.ntp.org, 1.ch.pool.ntp.org, 2.ch.pool.ntp.org, 3.ch.pool.ntp.org, ntp.myorg.org]
  servers:
    - 130.60.128.3
    - 130.60.64.51
    - 130.60.204.10

Kommandos ausführen mit runcmd. Apt nochmals aktualisieren und snapd deinstallieren:

runcmd:
  - apt update
  - apt purge snapd -y

Folgende Pakete installieren:

packages:
  - python-pip
  - htop
  - nginx
  - ansible

Und schlussendlich nochmals einige Kommandos ausführen:

runcmd:
  - pip install --upgrade pip
  - apt autoclean -y
  - apt autoremove -y
  - echo '<!DOCTYPE html><html><head><title>Welcome to Terraform test</title><style>body{width:35em;margin:0 auto;font-family:Tahoma,Verdana,Arial,sans-serif;}</style></head><body><h1>Welcome to Terraform test!</h1><p>Terraform AWS EC2 test...</p><p><a href="https://www.supportblog.ch/">www.supportblog.ch</a><br/></p><p><em>Thank you for trying.</em></p><iframe src="https://giphy.com/embed/vQqeT3AYg8S5O" width="480" height="330" frameBorder="0" class="giphy-embed" allowFullScreen></iframe><p>That was easy!?&nbsp;<a href="https://giphy.com/gifs/gets-smart-question-vQqeT3AYg8S5O">via GIPHY</a></p></p></body></html>' > /var/www/html/index.nginx-debian.html
  - systemctl reload nginx.service

Aufräumen, pip aktualisieren, einfache Standard Index-Datei für nginx erstellen und dann nginx reloaden.

Terraform: terraform plan

Mit terraform plan können wir nun sehen was Terraform ausführen würde. Terraform erstellt anhand der schon vorhandenen Ressourcen in der AWS und seiner jetzigen Konfiguration einen Ausführungsplan:

Terraform: terraform apply

Mit terraform apply führen wir die Schritte aus nach einer finalen Bestätigung mit yes:

Die Outputs von den folgenden Zeilen sind am Ende zu sehen:

# availability zone
output "availability_zone-name" {
  value = "${var.desired_availability_zone}"
}

# vpc id
output "vpc-id" {
  value = "${aws_vpc.vpc_terraform.id}"
}

# subnet id
output "subnet-id" {
  value = "${aws_subnet.subnet_public_terraform.id}"
}

# internet gateway id
output "gw-id" {
  value = "${aws_internet_gateway.gw_terraform.id}"
}
Outputs:

availability_zone-name = eu-central-1a
gw-id = igw-d755d2bf
subnet-id = subnet-e74ac58c
vpc-id = vpc-7711ea1c

AWS Management Console

Nun schauen wir uns das Terraform Resultat genauer an. In der AWS Management Console unter EC2 -> Instances sehen wir nun die erstellte EC2 Instanz:

Die erstellte Konfiguration der EC2:






Public DNS URL

Nach 3-5 Minuten sollte auch Cloud-init durchgelaufen sein und wir können den Public DNS Link im Browser aufrufen:

SSH Zugriff mit Public Key

Nun testen wir den Zugriff mit dem hinterlegten public key:

Cloud-init config

Schauen wir uns noch die cloud-init Konfiguration an:

Scheint alles bestens geklappt zu haben.

So, das war es auch schon. Ich hoffe die Konfiguration ist nützlich für jemanden.