add terraform modules and Makefile

This commit is contained in:
Jakub Sokołowski 2018-08-01 13:34:27 -04:00
commit d6a65b6444
No known key found for this signature in database
GPG Key ID: 4EF064D0E6D63020
15 changed files with 834 additions and 0 deletions

18
.gitignore vendored Normal file
View File

@ -0,0 +1,18 @@
.envrc
.terraform/
.terraform/environment
terraform.tfstate.d/
*.tfstate
*.tfstate.backup
*.tfvars
*.retry
ansible/files/secrets/*
ansible/secrets.yml
ansible/files/*.crt
ansible/files/*.srl
ansible/files/*.key
# Google Cloud secrets
google-cloud.json

77
Makefile Normal file
View File

@ -0,0 +1,77 @@
#!/usr/bin/env bash
OS = $(strip $(shell uname -s))
ARCH = linux_amd64
ifeq ($(OS),Darwin)
ARCH = darwin_amd64
endif
PLUGIN_DIR = ~/.terraform.d/plugins
PROVIDER_NAME = terraform-provider-ansible
PROVIDER_VERSION = v0.0.4
PROVIDER_ARCHIVE = $(PROVIDER_NAME)-$(ARCH).zip
PROVIDER_URL = https://github.com/nbering/terraform-provider-ansible/releases/download/$(PROVIDER_VERSION)/$(PROVIDER_ARCHIVE)
PROVISIONER_NAME = terraform-provisioner-ansible
PROVISIONER_VERSION = v0.0.1
PROVISIONER_TMP = /tmp/${PROVISIONER_NAME}
PROVISIONER_URL = https://github.com/radekg/${PROVISIONER_NAME}
PLATFORM = linux
ifeq ($(OS),Darwin)
PLATFORM = darwin
endif
all: requirements install-provider install-provisioner secrets cleanup
echo "Success!"
plugins: install-provider install-provisioner
requirements:
ansible-galaxy install --force -r ansible/requirements.yml
install-unzip:
ifeq (, $(shell which unzip)) \
$(error "No unzip in PATH, consider doing apt install unzip") \
endif
install-provider:
if [ ! -e $(PLUGIN_DIR)/$(ARCH)/$(PROVIDER_NAME)_$(PROVIDER_VERSION) ]; then \
mkdir -p $(PLUGIN_DIR); \
wget $(PROVIDER_URL) -P $(PLUGIN_DIR); \
unzip -o $(PLUGIN_DIR)/$(PROVIDER_ARCHIVE) -d $(PLUGIN_DIR); \
fi
install-provisioner:
if [ ! -e $(PLUGIN_DIR)/$(ARCH)/$(PROVISIONER_NAME)_$(PROVISIONER_VERSION) ]; then \
mkdir -p $(PROVISIONER_TMP); \
git clone $(PROVISIONER_URL) $(PROVISIONER_TMP); \
cd $(PROVISIONER_TMP); \
go get github.com/hashicorp/terraform; \
go get github.com/mitchellh/go-homedir; \
go get github.com/mitchellh/go-linereader; \
go get github.com/mitchellh/mapstructure; \
go get golang.org/x/crypto/ssh; \
go get github.com/satori/go.uuid; \
make install; \
make build-$(PLATFORM); \
fi
secrets:
pass services/consul/ca-crt > ansible/files/consul-ca.crt
pass services/consul/ca-key > ansible/files/consul-ca.key
pass services/consul/client-crt > ansible/files/consul-client.crt
pass services/consul/client-key > ansible/files/consul-client.key
pass cloud/GoogleCloud/json > google-cloud.json
echo "\
# secrets extracted from password-store\n\
digitalocean_token = \"$(shell pass cloud/DigitalOcean/token)\"\n\
cloudflare_token = \"$(shell pass cloud/Cloudflare/token)\"\n\
cloudflare_email = \"$(shell pass cloud/Cloudflare/email)\"\n\
alicloud_access_key = \"$(shell pass cloud/Alibaba/access-key)\"\n\
alicloud_secret_key = \"$(shell pass cloud/Alibaba/secret-key)\"\n\
" > terraform.tfvars
cleanup:
rm -rf $(PLUGIN_DIR)/$(ARCHIVE)

View File

@ -0,0 +1,124 @@
/* DERIVED --------------------------------------*/
locals {
stage = "${terraform.workspace}"
tokens = "${split(".", local.stage)}"
dc = "${var.provider}-${var.zone}"
/* always add SSH, Tinc, and Netdata to allowed ports */
open_ports = [
"22/22", "655/655", "8000/8000", "${var.open_ports}",
]
}
/* RESOURCES ------------------------------------*/
/* default vpc to avoid creating by hand */
data "alicloud_vpcs" "host" {
is_default = true
}
/* default vswitch to avoid creating by hand */
data "alicloud_vswitches" "host" {
is_default = true
}
resource "alicloud_security_group" "host" {
name = "sg-${var.env}-${local.stage}"
description = "Sec Group via Terraform"
vpc_id = "${data.alicloud_vpcs.host.vpcs.0.id}"
}
resource "alicloud_security_group_rule" "host" {
security_group_id = "${alicloud_security_group.host.id}"
type = "ingress"
ip_protocol = "all"
cidr_ip = "0.0.0.0/0"
port_range = "${replace(element(local.open_ports, count.index), "-", "/")}"
count = "${length(local.open_ports)}"
}
data "alicloud_images" "host" {
owners = "self"
name_regex = "${var.image}"
}
resource "alicloud_instance" "host" {
host_name = "${var.name}-${format("%02d", count.index+1)}.${local.dc}.${var.env}.${local.stage}"
instance_name = "${var.name}-${format("%02d", count.index+1)}.${local.dc}.${var.env}.${local.stage}"
security_groups = ["${alicloud_security_group.host.id}"]
image_id = "${data.alicloud_images.host.images.0.id}"
vswitch_id = "${data.alicloud_vswitches.host.vswitches.0.id}"
tags = {
stage = "${local.stage}"
group = "${var.group}"
env = "${var.env}"
}
key_name = "${var.key_pair}"
availability_zone = "${var.zone}"
instance_type = "${var.type}"
system_disk_category = "${var.disk}"
count = "${var.count}"
internet_max_bandwidth_out = "${var.max_band_out}"
/* costs */
instance_charge_type = "${var.charge}"
period_unit = "${var.period}"
/* necessary because alicloud does not provide it */
/* https://github.com/radekg/terraform-provisioner-ansible/issues/54 */
connection {
host = "${self.public_ip}"
}
/* bootstraping access for later Ansible use */
provisioner "ansible" {
local = "yes"
plays {
playbook = "${path.cwd}/ansible/bootstrap.yml"
groups = ["${var.group}"]
extra_vars = {
hostname = "${var.name}-${format("%02d", count.index+1)}.${local.dc}.${var.env}.${local.stage}"
ansible_ssh_user = "${var.ssh_user}"
data_center = "${local.dc}"
stage = "${local.stage}"
env = "${var.env}"
}
}
}
}
resource "alicloud_eip" "host" {
count = "${var.count}"
lifecycle = { prevent_destroy = true }
}
resource "alicloud_eip_association" "host" {
allocation_id = "${element(alicloud_eip.host.*.id, count.index)}"
instance_id = "${element(alicloud_instance.host.*.id, count.index)}"
count = "${var.count}"
}
resource "cloudflare_record" "host" {
domain = "${var.domain}"
count = "${var.count}"
name = "${element(alicloud_instance.host.*.host_name, count.index)}"
value = "${element(alicloud_eip.host.ip_address, count.index)}"
type = "A"
ttl = 3600
}
resource "ansible_host" "host" {
inventory_hostname = "${element(alicloud_instance.host.*.host_name, count.index)}"
groups = ["${var.group}", "${local.dc}"]
count = "${var.count}"
vars {
ansible_host = "${element(alicloud_eip.host.ip_address, count.index)}"
hostname = "${element(alicloud_instance.host.*.host_name, count.index)}"
region = "${element(alicloud_instance.host.*.availability_zone, count.index)}"
dns_entry = "${element(alicloud_instance.host.*.host_name, count.index)}.${var.domain}"
dns_domain = "${var.domain}"
data_center = "${local.dc}"
stage = "${local.stage}"
env = "${var.env}"
}
}

View File

@ -0,0 +1,7 @@
output "public_ips" {
value = ["${alicloud_eip.host.*.ip_address}"]
}
output "hostnames" {
value = ["${alicloud_instance.host.*.host_name}"]
}

View File

@ -0,0 +1,90 @@
/* SCALING --------------------------------------*/
variable image {
/**
* This image is created with Packer because Alicloud does not provide one
* See: https://github.com/status-im/infra-utils/tree/master/alicloud/ubuntu_1804
*/
description = "OS image used to create instance."
default = "ubuntu_18_04_64_custom_20180719"
}
variable type {
description = "Type of instance to create."
default = "ecs.t5-lc2m1.nano"
}
variable zone {
description = "Availability Zone in which the instance will be created."
default = "cn-hongkong-c"
}
variable disk {
description = "Disk I/O optimization type."
default = "cloud_ssd"
}
variable max_band_out {
description = "Maximum outgoing bandwidth to the public network, measured in Mbps."
default = 30
}
variable count {
description = "Number of instances to start in this region."
}
/* FIREWALL -------------------------------------*/
variable open_ports {
description = "Ports to enable access to through security group."
type = "list"
default = []
}
/* GENERAL --------------------------------------*/
variable provider {
description = "Short name of provider being used."
/* Digital Ocean */
default = "ac"
}
variable name {
description = "Prefix of hostname before index."
default = "node"
}
variable charge {
description = "Way in which the instance is paid for."
/* The other value is PrePaid */
default = "PostPaid"
}
variable period {
description = "Time period in which we pay for instances."
/* The other value is Week */
default = "Month"
}
variable group {
description = "Name of Ansible group to add hosts to."
}
variable env {
description = "Environment for these hosts, affects DNS entries."
}
variable domain {
description = "DNS Domain to update"
}
variable ssh_user {
description = "SSH user used to log in after creation."
default = "root"
}
variable key_pair {
description = "SSH key pair used to log in to instance"
/* WARNING I really shouldn't use my own key here */
default = "jakub_status.im"
}

View File

@ -0,0 +1,77 @@
/* DERIVED --------------------------------------*/
locals {
stage = "${terraform.workspace}"
dc = "${var.provider}-${var.region}"
}
/* RESOURCES ------------------------------------*/
# create a tag for every segment of workspace separate by dot
locals = {
tags = ["${local.stage}", "${var.group}", "${var.env}"]
tags_sorted = "${sort(distinct(local.tags))}"
tags_count = "${length(local.tags_sorted)}"
}
resource "digitalocean_tag" "host" {
name = "${element(local.tags_sorted, count.index)}"
count = "${local.tags_count}"
}
resource "digitalocean_droplet" "host" {
image = "${var.image}"
name = "${var.name}-${format("%02d", count.index+1)}.${local.dc}.${var.env}.${local.stage}"
region = "${var.region}"
size = "${var.size}"
count = "${var.count}"
ssh_keys = "${var.ssh_keys}"
tags = ["${digitalocean_tag.host.*.id}"]
provisioner "ansible" {
plays {
playbook = "${path.cwd}/ansible/bootstrap.yml"
groups = ["${var.group}"]
extra_vars = {
hostname = "${var.name}-${format("%02d", count.index+1)}.${local.dc}.${var.env}.${local.stage}"
ansible_ssh_user = "${var.ssh_user}"
data_center = "${local.dc}"
stage = "${local.stage}"
env = "${var.env}"
}
}
local = "yes"
}
}
resource "digitalocean_floating_ip" "host" {
droplet_id = "${element(digitalocean_droplet.host.*.id, count.index)}"
region = "${element(digitalocean_droplet.host.*.region, count.index)}"
count = "${var.count}"
lifecycle = { prevent_destroy = true }
}
resource "cloudflare_record" "host" {
domain = "${var.domain}"
count = "${var.count}"
name = "${element(digitalocean_droplet.host.*.name, count.index)}"
value = "${element(digitalocean_floating_ip.host.*.ip_address, count.index)}"
type = "A"
ttl = 3600
}
resource "ansible_host" "host" {
inventory_hostname = "${element(digitalocean_droplet.host.*.name, count.index)}"
groups = ["${var.group}", "${local.dc}"]
count = "${var.count}"
vars {
ansible_user = "admin"
ansible_host = "${element(digitalocean_floating_ip.host.*.ip_address, count.index)}"
hostname = "${element(digitalocean_droplet.host.*.name, count.index)}"
region = "${element(digitalocean_droplet.host.*.region, count.index)}"
dns_entry = "${element(digitalocean_droplet.host.*.name, count.index)}.${var.domain}"
nodes_per_host = "${var.nodes_per_host}"
eth_network = "${var.eth_network}"
data_center = "${local.dc}"
stage = "${local.stage}"
env = "${var.env}"
}
}

View File

@ -0,0 +1,3 @@
output "public_ip" {
value = ["${digitalocean_floating_ip.host.*.ip_address}"]
}

View File

@ -0,0 +1,67 @@
/* SCALING ---------------------------------------*/
variable count {
description = "Number of hosts to run."
}
variable nodes_per_host {
description = "Number of statsd containers to run per host."
}
variable size {
description = "Size of the hosts to deploy."
# cmd: doctl compute size list
default = "s-1vcpu-1gb"
}
variable region {
description = "Region in which to deploy hosts."
# cmd: doctl compute region list
default = "ams3"
}
variable image {
description = "OS image to use when deploying hosts."
# cmd: doctl compute image list --public
default = "ubuntu-18-04-x64"
}
variable provider {
description = "Short name of the provider used."
# DigitalOcean
default = "do"
}
/* GENERAL ---------------------------------------*/
variable name {
description = "Name for hosts. To be used in the DNS entry."
}
variable group {
description = "Ansible group to assign hosts to."
}
variable env {
description = "Environment for these hosts, affects DNS entries."
}
variable domain {
description = "DNS Domain to update"
}
variable eth_network {
description = "Ethereum network to connect to."
}
variable ssh_user {
description = "User used to log in to instance"
default = "root"
}
variable ssh_keys {
description = "Names of ssh public keys to add to created hosts"
type = "list"
# cmd: doctl compute ssh-key list
default = ["16822693", "18813432", "18813461", "19525749", "20671731", "20686611"]
}

View File

@ -0,0 +1,116 @@
/* DERIVED --------------------------------------*/
locals {
stage = "${terraform.workspace}"
dc = "${var.provider}-${var.zone}"
/* always add SSH, Tinc, and Netdata to allowed ports */
open_ports = ["22", "655", "8000", "${var.open_ports}"]
}
/* RESOURCES ------------------------------------*/
locals = {
tags = [
"${var.name}", "${local.stage}", "${var.env}",
/* for precise targeting with firewall rules */
"${var.name}-${var.env}-${local.stage}",
]
tags_sorted = "${sort(distinct(local.tags))}"
}
resource "google_compute_address" "host" {
name = "${var.name}-${format("%02d", count.index+1)}-${local.dc}-${var.env}-${local.stage}"
count = "${var.count}"
lifecycle = { prevent_destroy = true }
}
resource "google_compute_firewall" "host" {
name = "allow-${var.name}-${var.zone}-${var.env}-${local.stage}"
network = "default"
target_tags = ["${var.name}-${var.env}-${local.stage}"]
allow {
protocol = "tcp"
ports = ["${local.open_ports}"]
}
allow {
protocol = "udp"
ports = ["${local.open_ports}"]
}
}
resource "google_compute_instance" "host" {
name = "${var.name}-${format("%02d", count.index+1)}-${local.dc}-${var.env}-${local.stage}"
zone = "${var.zone}"
count = "${var.count}"
machine_type = "${var.machine_type}"
/* enable changing machine_type */
allow_stopping_for_update = true
tags = ["${local.tags_sorted}"]
boot_disk {
initialize_params {
image = "${var.image}"
}
}
network_interface {
network = "default"
access_config {
nat_ip = "${element(google_compute_address.host.*.address, count.index)}"
}
}
metadata {
node = "${var.name}"
env = "${var.env}"
group = "${var.group}"
/* This is a hack because we can't use dots in actual instance name */
hostname = "${var.name}-${format("%02d", count.index+1)}.${local.dc}.${var.env}.${local.stage}"
/* Enable SSH access */
sshKeys = "${var.ssh_user}:${file(var.ssh_key)}"
}
/* bootstrap access to host and basic resources */
provisioner "ansible" {
plays {
playbook = "${path.cwd}/ansible/bootstrap.yml"
groups = ["${var.group}"]
extra_vars = {
hostname = "${var.name}-${format("%02d", count.index+1)}.${local.dc}.${var.env}.${local.stage}"
ansible_ssh_user = "${var.ssh_user}"
data_center = "${local.dc}"
stage = "${local.stage}"
env = "${var.env}"
}
}
local = "yes"
}
}
resource "cloudflare_record" "host" {
domain = "${var.domain}"
count = "${var.count}"
name = "${element(google_compute_instance.host.*.metadata.hostname, count.index)}"
value = "${element(google_compute_instance.host.*.network_interface.0.access_config.0.assigned_nat_ip , count.index)}"
type = "A"
ttl = 3600
}
resource "ansible_host" "host" {
inventory_hostname = "${element(google_compute_instance.host.*.metadata.hostname, count.index)}"
groups = ["${var.group}", "${local.dc}"]
count = "${var.count}"
vars {
ansible_user = "admin"
ansible_host = "${element(google_compute_instance.host.*.network_interface.0.access_config.0.assigned_nat_ip , count.index)}"
hostname = "${element(google_compute_instance.host.*.metadata.hostname, count.index)}"
region = "${element(google_compute_instance.host.*.zone, count.index)}"
dns_entry = "${element(google_compute_instance.host.*.metadata.hostname, count.index)}.${var.domain}"
nodes_per_host = "${var.nodes_per_host}"
eth_network = "${var.eth_network}"
data_center = "${local.dc}"
stage = "${local.stage}"
env = "${var.env}"
}
}

View File

@ -0,0 +1,3 @@
output "public_ip" {
value = ["${google_compute_instance.host.*.network_interface.0.access_config.0.assigned_nat_ip }"]
}

View File

@ -0,0 +1,77 @@
/* SCALING ---------------------------------------*/
variable count {
description = "Number of hosts to run."
}
variable nodes_per_host {
description = "Number of statsd containers to run per host."
}
variable machine_type {
description = "Type of machine to deploy."
/* https://cloud.google.com/compute/docs/machine-types */
default = "n1-standard-1"
}
variable zone {
description = "Specific zone in which to deploy hosts."
/* https://cloud.google.com/compute/docs/regions-zones/ */
default = "us-central1-a"
}
variable image {
description = "OS image to use when deploying hosts."
/* https://cloud.google.com/compute/docs/images */
default = "ubuntu-os-cloud/ubuntu-1804-lts"
}
variable provider {
description = "Short name of the provider used."
/* Google Cloud */
default = "gc"
}
/* CONFIG ----------------------------------------*/
variable name {
description = "Name for hosts. To be used in the DNS entry."
}
variable env {
description = "Environment for these hosts, affects DNS entries."
}
variable group {
description = "Ansible group to assign hosts to."
}
variable domain {
description = "DNS Domain to update"
}
variable eth_network {
description = "Ethereum network to connect to."
default = 1
}
/* MODULE ----------------------------------------*/
variable ssh_user {
description = "User used to log in to instance"
default = "root"
}
variable ssh_key {
description = "Names of ssh public keys to add to created hosts"
/* TODO this needs to be dynamic */
default = "~/.ssh/status.im/id_rsa.pub"
}
/* FIREWALL -------------------------------------------*/
variable open_ports {
description = "Port ranges to enable access from outside. Format: 'N-N'"
type = "list"
default = []
}

View File

@ -0,0 +1,64 @@
/**
* Unfortunately Terraform does not support using the count parameter
* with custom modules, for more details see:
* https://github.com/hashicorp/terraform/issues/953
*
* Because of this to add a region/zone you have to copy a provider
* module and give it a different region/size argument.
*/
/* Digital Ocean */
module "digital-ocean-ams3" {
source = "../digital-ocean"
/* specific */
name = "${var.name}"
count = "${var.count}"
env = "${var.env}"
group = "${var.group}"
eth_network = "${var.eth_network}"
/* scaling */
size = "${var.do_size}"
nodes_per_host = "${var.nodes_per_host}"
region = "ams3"
/* general */
domain = "${var.domain}"
}
resource "cloudflare_record" "do-ams3" {
domain = "${var.domain}"
name = "nodes.do-ams3.${var.env}.${terraform.workspace}"
value = "${element(module.digital-ocean-ams3.public_ip, count.index)}"
count = "${var.count}"
type = "A"
ttl = 3600
}
/* Google Cloud */
module "google-cloud-us-central1" {
source = "../google-cloud"
/* specific */
name = "${var.name}"
count = "${var.count}"
env = "${var.env}"
group = "${var.group}"
eth_network = "${var.eth_network}"
/* scaling */
machine_type = "${var.gc_size}"
nodes_per_host = "${var.nodes_per_host}"
zone = "us-central1-a"
/* general */
domain = "${var.domain}"
/* firewall */
open_ports = "${var.open_ports}"
}
resource "cloudflare_record" "gc-us-central1" {
domain = "${var.domain}"
name = "nodes.gc-us-central1-a.${var.env}.${terraform.workspace}"
value = "${element(module.google-cloud-us-central1.public_ip, count.index)}"
count = "${var.count}"
type = "A"
ttl = 3600
}

View File

View File

@ -0,0 +1,54 @@
variable count {
description = "Number of hosts to run."
}
variable name {
description = "Environment for these hosts, affects DNS entries."
}
variable env {
description = "Environment for these hosts, affects DNS entries."
}
variable group {
description = "Ansible group to assign hosts to."
}
variable domain {
description = "DNS Domain to update"
}
variable eth_network {
description = "Ethereum network to connect to."
default = 1
}
variable nodes_per_host {
description = "Number of statsd containers to run per host."
default = 0
}
/* Scaling -------------------------------------------*/
variable do_size {
description = "Size of host to provision in Digital Ocean."
default = "s-1vcpu-1gb"
}
variable gc_size {
description = "Size of host to provision in Google Cloud."
default = "n1-standard-1"
}
variable ac_size {
description = "Size of host to provision in Google Cloud."
default = "ecs.t5-lc1m1.small"
}
/* Firewall -------------------------------------------*/
variable open_ports {
description = "Port ranges to enable access from outside. Format: 'N-N'"
type = "list"
default = []
}

57
variables.tf Normal file
View File

@ -0,0 +1,57 @@
/* CONFIG ----------------------------------------*/
variable eth_network {
description = "Ethereum network to connect to."
# Default to Mainnet: see geth/params/cluster.go in status-go
default = 1
}
variable ssh_keys {
description = "Names of ssh public keys to add to created hosts"
type = "list"
# ssh key IDs acquired using doctl
default = ["16822693", "18813432", "18813461", "19525749", "20671731", "20686611"]
}
variable env {
description = "Environment for these hosts, affects DNS entries."
default = "eth"
}
variable domain {
description = "DNS Domain to update"
default = "statusim.net"
}
variable ssh_user {
description = "User used to log in to instance"
default = "root"
}
/* PROVIDERS ------------------------------------*/
variable cloudflare_token {
description = "Token for interacting with Cloudflare API."
}
variable digitalocean_token {
description = "Token for interacting with DigitalOcean API."
}
variable cloudflare_email {
description = "Email address of Cloudflare account."
}
variable alicloud_access_key {
description = "Alibaba Cloud API access key."
}
variable alicloud_secret_key {
description = "Alibaba Cloud API secret key."
}
variable alicloud_region {
description = "Alibaba Cloud hosting region."
default = "cn-hongkong"
}