diff --git a/.gitignore b/.gitignore index a5cef80..a52fc5e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,16 @@ -devkey -devkey.pub +**/devkey +**/devkey.pub terraform.tfvars *.log -.terraform/ +**/.terraform/ vendor/ .lwapi.toml terraform-provider-liquidweb terraform.tfstate* +*.tfvars +*.tfvars.json go/ .cache/ dist/ .ash_history +.terraform.lock.hcl diff --git a/docs/index.md b/docs/index.md index 838d2c2..d1895bb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -43,6 +43,6 @@ resource "liquidweb_cloud_server" "server" { ## Argument Reference - `LWAPI_USERNAME` -- `LWAPI_PASSWORD`` +- `LWAPI_PASSWORD` These environment variables are used to set your credentials for the provider. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..ad0c06a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,86 @@ +# Terraform Examples + +This repository contains terraform examples that are slated for inclusion in the terraform repository elsewhere, but not yet ready. + +## What is Terraform? + +Terraform is a tool targeted at a Infrastructure as Code approach to managing asset inventory. +It offers a declarative language to create configurations describing infrastructure. +Given the configurations, you can rapidly create, remove, and recreate infrastructure. +Since the configuraitons are plaintext, it allows easy versioning of the infrastructure state with Version Control Software. + +### Background Terms + +That's a loaded paragraph, some terminology: + +- Version Control Software (vcs) - like `git`, a tool that lets you track files over time and compare differences + - Of note, most developers put their source code in VCS. This has many benefits. +- Infrastructure as Code (IaC) - managing servers via config files, often which you can commit to a repository +- Declarative Syntax / Language - describing what an system should be +- Asset Inventory - what assets you have. VPS's are an asset, but SSL certificates, LB's, and Block Storage are also assets. +- configuration files end in `.tf` and determine what is needed +- State - the current way a system is, the actual live snapshot of it, not the way it hsould be +- Lockfile - a file tracking what things terraform currently has + +### Terraform Basic Commands + +The focus of Terraform is create, recreate, and destroying what is needed. +Terraform can be used alone, and assets recreated as your schema changes. +But most of the time, multiple IaC tools are used to better describe a system. + +The major background pieces it will create are: + +- the lock file resides at `./.terraform.lock.hcl` +- the state file resides at `./terraform.tfstate` +- a backup state file at `./terraform.tfstate.backup` +- providers typically reside in `./terraform.d` + +If you have something deployed, you want to save the + +The major commands that terraform provides are: + +- `init` - download required providers and set up state and lockfile +- `validate` - make sure configs are valid +- `plan` - show changes to modify state to match configs +- `apply` - run `plan`, then prompt to make those changes +- `destroy` show changes to remove everything, prompt, then remove everything +- `show` - display the current assets +- `taint` - mark an asset currently deployed, on next `apply` will be recreated +- `refresh` - update the state of assets (not supported with LiquidWeb's provider) +- `import` - add existing assets into current state (not supported with LiquidWeb's provider) + +### Installing and Examples + +The [Hashicorp official instructions for installing terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) are well written. +You are likely best off going there. +Do note, you will likely have a better time if you use the package install for your OS. +In other words, on macOS use `homebrew`, on Windows use `choco`, on Linux use your package manager. + +The LiquidWeb provider does not require special installation. +If it is used in your configs, it should be automatically installed with `terraform init`. + +[Documentation for that provider is published on the provider page](https://registry.terraform.io/providers/liquidweb/liquidweb/latest/docs). + +You will need to provide credentials to a LiquidWeb account in order to use the LiquidWeb provider. +These credentials should be in the following environment variables: + +```env +LWAPI_USERNAME="username" +LWAPI_PASSWORD="password" +``` + +There is also an `acme` SSL provider. +If your domain is hosted with LiquidWeb, you can use this to get an SSL. +[The documentation gives a basic example](https://registry.terraform.io/providers/vancluever/acme/latest/docs/guides/dns-providers-liquidweb). +If you wish to get an SSL with the `acme` provider with a DNS server, you must provide the following credentials: + +```env +LIQUID_WEB_USERNAME="username" +LIQUID_WEB_PASSWORD="password" +``` + +For examples, please look at: + +- [Basic server example](./basic-example/) +- [Basic wordpress example](./simple-wordpress/) +- [Wordpress Cluster example](./scalable-wordpress/) diff --git a/examples/blockstorage.tf b/examples/basic-example/blockstorage.tf similarity index 73% rename from examples/blockstorage.tf rename to examples/basic-example/blockstorage.tf index a0e27f4..e7340e1 100644 --- a/examples/blockstorage.tf +++ b/examples/basic-example/blockstorage.tf @@ -3,7 +3,7 @@ resource "random_id" "block" { } resource "liquidweb_cloud_block_storage" "testing_block_volume" { - domain = "terraform-block${random_id.block.dec}.us-midwest-2.hostbaitor.com" + domain = "terraform-block${random_id.block.dec}.us-midwest-2.example.com" size = 10 } diff --git a/examples/dns.tf b/examples/basic-example/dns.tf similarity index 70% rename from examples/dns.tf rename to examples/basic-example/dns.tf index 6087d07..c5faf7c 100644 --- a/examples/dns.tf +++ b/examples/basic-example/dns.tf @@ -3,10 +3,10 @@ resource "random_id" "dns_rec" { } resource "liquidweb_network_dns_record" "testing_a_record" { - name = "dns-rec-${random_id.dns_rec.hex}.us-midwest-2.hostbaitor.com" + name = "dns-rec-${random_id.dns_rec.hex}.us-midwest-2.example.com" type = "A" rdata = "127.0.0.1" - zone = "hostbaitor.com" + zone = "example.com" } output "domain_a_name" { diff --git a/examples/loadbalancer.tf b/examples/basic-example/loadbalancer.tf similarity index 89% rename from examples/loadbalancer.tf rename to examples/basic-example/loadbalancer.tf index 883c4ed..af20911 100644 --- a/examples/loadbalancer.tf +++ b/examples/basic-example/loadbalancer.tf @@ -1,6 +1,6 @@ resource "liquidweb_network_load_balancer" "testing_lb" { depends_on = [data.liquidweb_network_zone.testing_zone] - name = "lb.0.terraform-testing.api.hostbaitor.com" + name = "lb.0.terraform-testing.api.example.com" region = data.liquidweb_network_zone.testing_zone.region_id diff --git a/examples/basic-example/provider.tf b/examples/basic-example/provider.tf new file mode 100644 index 0000000..117139c --- /dev/null +++ b/examples/basic-example/provider.tf @@ -0,0 +1,9 @@ + +terraform { + required_providers { + liquidweb = { + source = "local.providers/liquidweb/liquidweb" + version = "~> 1.6.2" + } + } +} diff --git a/examples/basic-example/readme.md b/examples/basic-example/readme.md new file mode 100644 index 0000000..7bf030f --- /dev/null +++ b/examples/basic-example/readme.md @@ -0,0 +1,3 @@ +# Basic Example + +This most basic Terraform example in this folder does not target a specific use case, instead it just creates resources. diff --git a/examples/server.tf b/examples/basic-example/server.tf similarity index 83% rename from examples/server.tf rename to examples/basic-example/server.tf index e68b323..e357191 100644 --- a/examples/server.tf +++ b/examples/basic-example/server.tf @@ -6,12 +6,12 @@ resource "random_id" "server" { resource "liquidweb_cloud_server" "testing_servers" { count = 2 - #config_id = "${data.liquidweb_cloud_server_config.api.id}" + #config_id = "${data.liquidweb_storm_server_config.api.id}" config_id = 1757 zone = 27 #data.liquidweb_network_zone.api.id template = "UBUNTU_1804_UNMANAGED" // ubuntu 18.04 - domain = "terraform-host${random_id.server[count.index].dec}.us-midwest-2.hostbaitor.com" + domain = "terraform-host${random_id.server[count.index].dec}.us-midwest-2.example.com" public_ssh_key = file("${path.root}/devkey.pub") password = "1Aaaaaaaaa" } diff --git a/examples/zone.tf b/examples/basic-example/zone.tf similarity index 100% rename from examples/zone.tf rename to examples/basic-example/zone.tf diff --git a/examples/provider.tf b/examples/provider.tf deleted file mode 100644 index 62f3474..0000000 --- a/examples/provider.tf +++ /dev/null @@ -1,16 +0,0 @@ -variable "liquidweb_config_path" { - type = string -} - -terraform { - required_providers { - liquidweb = { - source = "registry.terraform.io/liquidweb/liquidweb" - version = "~> 1.6.2" - } - } -} - -provider "liquidweb" { - config_path = var.liquidweb_config_path -} \ No newline at end of file diff --git a/examples/scalable-wordpress/data.tf b/examples/scalable-wordpress/data.tf new file mode 100644 index 0000000..c9fb499 --- /dev/null +++ b/examples/scalable-wordpress/data.tf @@ -0,0 +1,89 @@ +terraform { + required_providers { + liquidweb = { + source = "liquidweb/liquidweb" + version = ">= 1.7.0" + } + acme = { + source = "vancluever/acme" + version = "2.17.1" + } + } +} + +resource "random_id" "server" { + byte_length = 1 + # count = 1 +} + +resource "random_password" "server" { + length = 20 + special = false +} + +resource "random_password" "wordpress_dbpass" { + length = 20 + special = true +} + +resource "random_password" "wordpress_salt" { + length = 32 + special = true +} + +data "liquidweb_network_zone" "zonec" { + name = "Zone C" + region_name = "US Central" +} + +data "template_file" "install-wordpress" { + template = file("${path.module}/templates/install-wordpress.sh") + vars = { + user = var.username + } +} + +data "template_file" "wp-config" { + template = file("${path.module}/templates/wp-config.php") + vars = { + dbname = var.wordpress_dbname + dbuser = var.wordpress_dbuser + dbpass = random_password.wordpress_dbpass.result + salt = random_password.wordpress_salt.result + dbhost = liquidweb_cloud_server.dbserver.ip + } +} + +data "template_file" "site-conf" { + template = file("${path.module}/templates/nginx.conf") + vars = { + domain = var.site_name + user = var.username + } +} + +data "template_file" "php-conf" { + template = file("${path.module}/templates/php-fpm.conf") + vars = { + user = var.username + } +} + +resource "liquidweb_network_dns_record" "webserver_dns" { + count = 3 + name = liquidweb_cloud_server.webserver[count.index].domain + type = "A" + rdata = liquidweb_cloud_server.webserver[count.index].ip + zone = var.top_domain +} + +resource "liquidweb_network_dns_record" "wordpress_record" { + name = var.site_name + type = "A" + rdata = liquidweb_network_load_balancer.loadbalancer.vip + zone = var.top_domain +} + +output "domain_a_name" { + value = liquidweb_network_dns_record.wordpress_record.name +} diff --git a/examples/scalable-wordpress/dbserver.tf b/examples/scalable-wordpress/dbserver.tf new file mode 100644 index 0000000..325e302 --- /dev/null +++ b/examples/scalable-wordpress/dbserver.tf @@ -0,0 +1,43 @@ +resource "liquidweb_cloud_server" "dbserver" { + #config_id = "${data.liquidweb_storm_server_config.api.id}" + config_id = 1757 + zone = data.liquidweb_network_zone.zonec.network_zone_id + #data.liquidweb_network_zone.api.id + template = "ROCKYLINUX_8_UNMANAGED" + domain = "wordpress-db01-p${random_id.server.dec}.us-midwest-2.${var.top_domain}" + public_ssh_key = file("${path.root}/default.pub") + password = random_password.server.result + + connection { + type = "ssh" + user = "root" + agent = true + host = self.ip + } + + provisioner "remote-exec" { + inline = [ + "yum install -y epel-release", + "yum install -y http://rpms.remirepo.net/enterprise/remi-release-8.rpm", + "yum install -y wget curl mysql mysql-common mysql-server" + ] + } + + provisioner "remote-exec" { + inline = [ + "systemctl start mysqld.service", + "systemctl enable mysqld.service", + "firewall-cmd --zone public --permanent --add-port 3306/tcp", + "firewall-cmd --reload" + ] + } +} + +output "dbserver_hostnames" { + value = liquidweb_cloud_server.dbserver.ip +} + +output "dbserver_ips" { + value = liquidweb_cloud_server.dbserver.ip +} + diff --git a/examples/scalable-wordpress/default.pub b/examples/scalable-wordpress/default.pub new file mode 100644 index 0000000..eba69ae --- /dev/null +++ b/examples/scalable-wordpress/default.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDWMKoQLgiAhw3ZZrCd7pZfQ/Wqj+ct6jvV/+MzOJ2BLgoUd9ijnOImf/HWUwuWDol8vQP5sf5Q7mMURut2uFm7bLcZMVnZ7wWSUhS8qptdJY/Hixc3S65xP/DCRjKuE1YWbZXVZYPzLt7JERY3jADo7C0XjOzn5jY509RAU77EIm/idKT0Q0S382OjP8avNNfoevZ2h8A5EiRlcypCTsn8VJk6NFIk+NnqxWsxuCHCbt7+91hPvIhPFxW8f0y4TujIZyUc/8OqKqqBb7e3/eBEBfrCcDRVAho3vD7kLt42xdKy0BtpK58Dds3iToPjfho342qflaAoniXpeAkh0nX2PBJXN6t698bfJ+TVr5xChZ3J7FJw/G7D6KI01hdr43ACTILpDb1gpWHgZMTt7/G2PE3T6hPZeZDYVce5HHkn8B1ptZtRr3vbKZLsPLTm5lEK3GayOWxuUpBRvYaqQNOHw7lOlGTlYJGlCfbfuI2bFQxPBLhaA/KlTiPT5MIGganJyD82gIf4Yw4UckpV57AP7KAP64D9++a5fmpVQ4lBWE7DP+GoXUz8yw9YMygBbniEbkM6dXf6RB8G+8GxHohfZ/lFgWUEyrrL9vrFi5eELTCpoPu5COzXwMg61cXKfsGD8/n3Eh8XSMTFeJQJdRmPQCBs8SormvT6j5JhGUOVyw== cardno:9_021_958 diff --git a/examples/scalable-wordpress/loadbalancer.tf b/examples/scalable-wordpress/loadbalancer.tf new file mode 100644 index 0000000..5283481 --- /dev/null +++ b/examples/scalable-wordpress/loadbalancer.tf @@ -0,0 +1,26 @@ +resource "liquidweb_network_load_balancer" "loadbalancer" { + # depends_on = [data.liquidweb_network_zone.testing_zone] + name = "wordpress-loadbalancer1-p${random_id.server.dec}${var.top_domain}" + + region = data.liquidweb_network_zone.zonec.region_id + + nodes = liquidweb_cloud_server.webserver[*].ip + + service { + src_port = 80 + dest_port = 80 + } + + service { + src_port = 443 + dest_port = 443 + } + + #session_persistence = false + #ssl_termination = false + strategy = "roundrobin" +} + +output "lb_vip" { + value = liquidweb_network_load_balancer.loadbalancer.vip +} \ No newline at end of file diff --git a/examples/scalable-wordpress/readme.md b/examples/scalable-wordpress/readme.md new file mode 100644 index 0000000..9da952c --- /dev/null +++ b/examples/scalable-wordpress/readme.md @@ -0,0 +1,331 @@ +# Simple Wordpress Terraform Example + +The files in this directoy provide a simple wordpress terraform deployment. +This deployment is not intended for production - there are no backups, there's a few quirks, but it could be used as-is and makes a good starting point. +This page attempts to explain how the parts fit together. + + +* [Pre-Requisites](#pre-requisites) +* [Files in deployment](#files-in-deployment) + * [`vars.tf`](#varstf) + * [`data.tf`](#datatf) + * [`ssl.tf`](#ssltf) + * [`server.tf`](#servertf) + + + + +## Pre-Requisites + +* You must already have a Liquid Web account. +* Your Liquid Web account or API user must be set to environment variables: + * `LWAPI_USERNAME` - your account username + * `LWAPI_PASSWORD` - your account username +* For the ACME TLS provider, your account must be set to + * `LIQUID_WEB_USERNAME` - your account username (for ACME) + * `LIQUID_WEB_PASSWORD` - your account username (for ACME) + * `LIQUID_WEB_URL` set to `"https://api.liquidweb.com"` + * `LIQUID_WEB_ZONE` set to your domain name +* You must have a DNS zone created for the domain you want to use +* You should create a `.tfvars` to change the domain - see below + +Once you have those prerequisites, deploy from this directory with: + +```bash +terraform init +terraform apply +``` + +Tear this down with: + +```bash +terraform destroy +``` + +## Files in deployment + +This example is made up of 6 files in an attempt to simplify the example. +Technically, this could be in one `.tf` file instead. +The files are mostly similar to the `simple-wordpress` example. +Below, the content in each file is explained. + +`output` sections in each file simply show things at the end. +They do not impact the deployment, just the output you see. +Relevant useful pieces are shown from each item. + +```hcl +output "instances" { + value = liquidweb_cloud_server.webserver.*.ip +} +``` + +### `vars.tf` + +This is the first file, and likely the simplest, it just defines some default variables. +The two things that you likely only want to change are: + +```hcl +variable "site_name" { + type = string + default = "simple.example.com" +} + +variable "top_domain" { + type = string + default = "example.com" +} +``` + +* If you copy these files somewhere else, you can modify those values +* You can change them at cli `terraform apply -var "top_domain=example.com"` +* Environment variables - `export TF_VAR_TOP_DOMAIN="example.com"` +* We'd recommend using a [tfvars file](https://developer.hashicorp.com/terraform/language/values/variables#variable-definitions-tfvars-files) + +```hcl +top_domain = "example.com" +site_name = "wordpress.example.com" +``` + +### `data.tf` + +This file is focused on setting up the provider and a lot of pieces of data used elsewhere. + +First, the required providers are defined, that section is below. +This is used when you run `terraform init`. + +```hcl +terraform { + required_providers { + liquidweb = { + source = "liquidweb/liquidweb" + version = ">= 1.7.0" + } + acme = { + source = "vancluever/acme" + version = "2.17.1" + } + } +} +``` + +Next a bunch of random passwords and similar are generated: + +```hcl +resource "random_password" "server" { + length = 20 + special = false +} +``` + +Finally, templates are rendered. +Templates can be rendered inline, but here they are made into objects so terraform will detect changes. + +* `template` is a path to the location of the template (relative to the cwd) +* `vars` is a hash of variables available to be used in the template + +```hcl +data "template_file" "wp-config" { + template = file("${path.module}/templates/wp-config.php") + vars = { + dbhost = var.wordpress_dbhost + dbname = var.wordpress_dbname + dbuser = var.wordpress_dbuser + dbpass = random_password.wordpress_dbpass.result + salt = random_password.wordpress_salt.result + } +} +``` + +Finally, there are a few DNS records created for both the site and the server. +These are dependent on the server being created, and happen after that. +But `terraform` automatically creates the DNS records for ease of use. + +* `name` is where the DNS record is created +* `rdata` is what the DNS record should point to +* `zone` is the zone to create the DNS record in + +All of these are string fields, and can be changed to be whatever else is needed. + +```hcl +resource "liquidweb_network_dns_record" "server_dns" { + name = liquidweb_cloud_server.webserver.domain + type = "A" + rdata = liquidweb_cloud_server.webserver.ip + zone = var.top_domain +} +``` + +### `ssl.tf` + +`ssl.tf` incrementally creates the ssl, then outputs the domain name for the SSL. +With this method of SSL generation, you will need to run `terraform apply` to renew. + +Relevant links for this section: + +* [`acme_certificate` provider](https://registry.terraform.io/providers/vancluever/acme/latest/docs/resources/certificate#using-dns-challenges) +* [`liquidweb` challenge for use with zones hosted with LiquidWeb](https://registry.terraform.io/providers/vancluever/acme/latest/docs/guides/dns-providers-liquidweb) +* [source for the `terraform` provider (uses `lego`)](https://github.com/vancluever/terraform-provider-acme/tree/main/acme) +* [docs for `liquidweb` part within `lego`](https://go-acme.github.io/lego/dns/liquidweb/) +* [source for the `liquidweb` DNS challenge within `lego`](https://github.com/go-acme/lego/tree/master/providers/dns/liquidweb) +* [Let's Encrypt Endpoints](https://letsencrypt.org/docs/acme-protocol-updates/#api-endpoints) + +First, the Let's Encrypt URL is configured. + +```hcl +provider "acme" { + server_url = "https://acme-v02.api.letsencrypt.org/directory" +} + +Next a private key is created, and used to register with Let's Encrypt: +```hcl +resource "tls_private_key" "private_key" { + algorithm = "RSA" +} + +resource "acme_registration" "reg" { + account_key_pem = tls_private_key.private_key.private_key_pem + email_address = "nobody@${var.site_name}" +} +``` + +An SSL is requested, and the `liquidweb` DNS challenge is used. + +```hcl +resource "acme_certificate" "web_cert" { + account_key_pem = acme_registration.reg.account_key_pem + common_name = "${var.site_name}" + key_type = "4096" + +# subject_alternative_names = ["www2.example.com"] + + dns_challenge { + provider = "liquidweb" + } +} +``` + +This produces a `acme_certificate.web_cert` resource for use later. + +### `dbserver.tf` + +This is the simplest server of the bunch. +This wordpress cluster is backed by a single database server. + +Since this is the first server in this example, the following fields are given. + +* `zone` (required) determines which zone to create the server in +* `config_id` (required) is the numeric id of the server type to create +* `template` (required) is the base image to use +* `domain` (required) is the server's hostname +* `password` (optional) will be the server's root passwordot provided +* `public_ssh_key` (optional) an SSH key for root +* `lifecycle.create_before_desetroy` determines what to do when recreating a server` + +The config for this server does not do much, it mostly just installs MySQL. +After the cluster is bootstrapped, you need to go back and create the database. +You also need to allow access from the webservers. + +### `webserver.tf` + +The file `webserver.tf` contains the webservers. +This resource depends on random values, templates, the TLS cert, and an SSH key. + +First, the server options are specified - this determines how the server is created. + +* `zone` (required) determines which zone to create the server in +* `config_id` (required) is the numeric id of the server type to create +* `template` (required) is the base image to use +* `domain` (required) is the server's hostname +* `password` (optional) will be the server's root passwordot provided +* `public_ssh_key` (optional) an SSH key inserted for root +* `lifecycle.create_before_desetroy` determines what to do when recreating a server` + +You need either `public_ssh_key` or `password`, but not both. +If no `password` is provide, a random one will be set. + +```hcl + zone = data.liquidweb_network_zone.zonec.network_zone_id + config_id = 1757 + template = "ROCKYLINUX_8_UNMANAGED" + domain = "wordpress-host${random_id.server.dec}.us-midwest-2.${var.top_domain}" + public_ssh_key = file("${path.root}/default.pub") + password = random_password.server.result + + lifecycle { + create_before_destroy = false + } +``` + +Next, the connection to that server is configured. +The SSH key at `default.pub` should be replaced with your pub key, then this should use that key. +If you would rather, you can use the root password or [see the docs](https://developer.hashicorp.com/terraform/language/resources/provisioners/connection). + +```hcl + connection { + type = "ssh" + user = "root" + agent = true + host = self.ip + } +``` + +Once the server is online and the connection is good, some commands are run: + +```hcl + provisioner "remote-exec" { + inline = [ + "yum install -y epel-release", + "yum install -y http://rpms.remirepo.net/enterprise/remi-release-8.rpm", + "yum install -y wget curl nginx mysql mysql-common php82-php-fpm php82-php-mysqlnd php82-php-mbstring" + ] + } +``` + +Some files are also written to the vm. +All files here are from templates, but direct files could be done as well. +But see the [`file provisioner`](https://developer.hashicorp.com/terraform/language/resources/provisioners/file) docs and you can use a straight file instead as well. + +```hcl + provisioner "file" { + content = data.template_file.site-conf.rendered + destination = "/etc/nginx/conf.d/site.conf" +``` + +Once all of those are done, the server's up and ready to go through the Wordpress setup. + +### `loadbalancer.tf` + +File simply sets up a loadbalancer service to sit in front of the webservers. +The resource for this one is rather simple. + +* `name` specifies the name for the service - what it shows up as in your account. +* `nodes` determines the IP addresses that it routes to +* `service` blocks, one per each mapping, denotes external IPs ot internal addresses +* `strategy` is the balancing stragety, you probalby want `roundrobin` + +That resource then provides a `vip` asset, a DNS record for the site's domain is then created pointing to that `vip`. + +## Manual Bits & Caveats + +If you bootstrap this example, the database will not be set up. +In order to get this working, you need to SSH into one of the webserver nodes, and cat `/home/wordpress/www/wp-config.php`. +Then, with the username and password and database name from that file, you need to SSH to the database server. +You'll need to enter MySQL and run the following query to create the database, then the subsequent query for each webserver to set up access. + +```sql +CREATE DATABASE ${dbname} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +CREATE USER '${dbuser}'@'${webserver}' IDENTIFIED BY '${dbpass}'; +GRANT ALL PRIVILEGES ON ${dbname}.* TO '${dbuser}'@'${webserver}'; +``` + +The last 2 lines have to be repeated for every webserver. +And the password, username, and database name can be found in `wp-config.php`. + +This does not include any file replication for the `wp-content` directory. +This is actually not so much about setting up a wordpress install, as it is showing how to set up any install. + +For a production install, it's also recommended to further harden the firewall. +Redirecting traffic for `wp-admin` and syncing `wp-content` across nodes is also typical for a wordpress install (not implemented here). diff --git a/examples/scalable-wordpress/ssl.tf b/examples/scalable-wordpress/ssl.tf new file mode 100644 index 0000000..5d8e396 --- /dev/null +++ b/examples/scalable-wordpress/ssl.tf @@ -0,0 +1,27 @@ +provider "acme" { + server_url = "https://acme-v02.api.letsencrypt.org/directory" +} + +resource "tls_private_key" "private_key" { + algorithm = "RSA" +} + +resource "acme_registration" "reg" { + account_key_pem = tls_private_key.private_key.private_key_pem + email_address = "nobody@${var.site_name}" +} + +resource "acme_certificate" "web_cert" { + account_key_pem = acme_registration.reg.account_key_pem + common_name = "${var.site_name}" + key_type = "4096" + # subject_alternative_names = ["www2.example.com"] + + dns_challenge { + provider = "liquidweb" + } +} + +output "ssl" { + value = acme_certificate.web_cert.certificate_domain +} \ No newline at end of file diff --git a/examples/scalable-wordpress/templates/install-wordpress.sh b/examples/scalable-wordpress/templates/install-wordpress.sh new file mode 100755 index 0000000..e735b8a --- /dev/null +++ b/examples/scalable-wordpress/templates/install-wordpress.sh @@ -0,0 +1,12 @@ +groupadd ${user} +useradd -g ${user} ${user} + +mkdir -p /home/${user}/www +chmod g+x /home/${user} +usermod nginx -G wordpress + +curl --silent https://wordpress.org/latest.tar.gz|tar -C /home/${user}/www --strip-components=1 -xz 'wordpress/*' + +mv /root/wp-config.php /home/${user}/www/ + +chown -R wordpress. /home/wordpress/www \ No newline at end of file diff --git a/examples/scalable-wordpress/templates/nginx.conf b/examples/scalable-wordpress/templates/nginx.conf new file mode 100644 index 0000000..b2f408a --- /dev/null +++ b/examples/scalable-wordpress/templates/nginx.conf @@ -0,0 +1,41 @@ + +server { + listen 80; + listen 443 ssl; + server_name ${domain}; + ssl_certificate /etc/pki/tls/certs/${domain}.crt; + ssl_certificate_key /etc/pki/tls/private/${domain}.key; + + root /home/${user}/www; + index index.php index.html index.htm; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + client_max_body_size 500M; + + location / { + try_files $uri $uri/ /index.php?$args; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { + expires max; + log_not_found off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location ~ \.php$ { + fastcgi_pass unix:/var/opt/remi/php82/run/php-fpm/${user}.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } +} \ No newline at end of file diff --git a/examples/scalable-wordpress/templates/php-fpm.conf b/examples/scalable-wordpress/templates/php-fpm.conf new file mode 100644 index 0000000..867350d --- /dev/null +++ b/examples/scalable-wordpress/templates/php-fpm.conf @@ -0,0 +1,15 @@ +[${user}] +pm = ondemand +pm.max_children = 30 +pm.start_servers = 2 +pm.min_spare_servers = 2 +pm.max_spare_servers = 10 +pm.process_idle_timeout = 10s + +user = ${user} +group = ${user} + +listen = /var/opt/remi/php82/run/php-fpm/wordpress.sock +listen.owner = nginx +listen.group = nginx +listen.mode = 0660 diff --git a/examples/scalable-wordpress/templates/wp-config.php b/examples/scalable-wordpress/templates/wp-config.php new file mode 100644 index 0000000..5a7159e --- /dev/null +++ b/examples/scalable-wordpress/templates/wp-config.php @@ -0,0 +1,96 @@ += 1.7.0" + } + acme = { + source = "vancluever/acme" + version = "2.17.1" + } + } +} + +resource "random_id" "server" { + byte_length = 1 + # count = 1 +} + +resource "random_password" "server" { + length = 20 + special = false +} + +resource "random_password" "wordpress_dbpass" { + length = 20 + special = true +} + +resource "random_password" "wordpress_salt" { + length = 32 + special = true +} + +data "liquidweb_network_zone" "zonec" { + name = "Zone C" + region_name = "US Central" +} + +data "template_file" "install-wordpress" { + template = file("${path.module}/templates/install-wordpress.sh") + vars = { + user = var.username + } +} + +data "template_file" "wp-config" { + template = file("${path.module}/templates/wp-config.php") + vars = { + dbhost = var.wordpress_dbhost + dbname = var.wordpress_dbname + dbuser = var.wordpress_dbuser + dbpass = random_password.wordpress_dbpass.result + salt = random_password.wordpress_salt.result + } +} + +data "template_file" "site-conf" { + template = file("${path.module}/templates/nginx.conf") + vars = { + domain = var.site_name + user = var.username + } +} + +data "template_file" "php-conf" { + template = file("${path.module}/templates/php-fpm.conf") + vars = { + user = var.username + } +} + +data "template_file" "create-database" { + template = file("${path.module}/templates/create-database.sql") + vars = { + dbhost = var.wordpress_dbhost + dbname = var.wordpress_dbname + dbuser = var.wordpress_dbuser + dbpass = random_password.wordpress_dbpass.result + } +} + +resource "liquidweb_network_dns_record" "server_dns" { + name = liquidweb_cloud_server.simple_server.domain + type = "A" + rdata = liquidweb_cloud_server.simple_server.ip + zone = var.top_domain +} + +resource "liquidweb_network_dns_record" "wordpress_record" { + name = var.site_name + type = "A" + rdata = liquidweb_cloud_server.simple_server.ip + zone = var.top_domain +} + +output "domain_a_name" { + value = liquidweb_network_dns_record.wordpress_record.name +} + +output "server_hostname" { + value = liquidweb_network_dns_record.server_dns.name +} \ No newline at end of file diff --git a/examples/simple-wordpress/default.pub b/examples/simple-wordpress/default.pub new file mode 100644 index 0000000..eba69ae --- /dev/null +++ b/examples/simple-wordpress/default.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDWMKoQLgiAhw3ZZrCd7pZfQ/Wqj+ct6jvV/+MzOJ2BLgoUd9ijnOImf/HWUwuWDol8vQP5sf5Q7mMURut2uFm7bLcZMVnZ7wWSUhS8qptdJY/Hixc3S65xP/DCRjKuE1YWbZXVZYPzLt7JERY3jADo7C0XjOzn5jY509RAU77EIm/idKT0Q0S382OjP8avNNfoevZ2h8A5EiRlcypCTsn8VJk6NFIk+NnqxWsxuCHCbt7+91hPvIhPFxW8f0y4TujIZyUc/8OqKqqBb7e3/eBEBfrCcDRVAho3vD7kLt42xdKy0BtpK58Dds3iToPjfho342qflaAoniXpeAkh0nX2PBJXN6t698bfJ+TVr5xChZ3J7FJw/G7D6KI01hdr43ACTILpDb1gpWHgZMTt7/G2PE3T6hPZeZDYVce5HHkn8B1ptZtRr3vbKZLsPLTm5lEK3GayOWxuUpBRvYaqQNOHw7lOlGTlYJGlCfbfuI2bFQxPBLhaA/KlTiPT5MIGganJyD82gIf4Yw4UckpV57AP7KAP64D9++a5fmpVQ4lBWE7DP+GoXUz8yw9YMygBbniEbkM6dXf6RB8G+8GxHohfZ/lFgWUEyrrL9vrFi5eELTCpoPu5COzXwMg61cXKfsGD8/n3Eh8XSMTFeJQJdRmPQCBs8SormvT6j5JhGUOVyw== cardno:9_021_958 diff --git a/examples/simple-wordpress/readme.md b/examples/simple-wordpress/readme.md new file mode 100644 index 0000000..be8777b --- /dev/null +++ b/examples/simple-wordpress/readme.md @@ -0,0 +1,277 @@ +# Simple Wordpress Terraform Example + +The files in this directoy provide a simple wordpress terraform deployment. +This deployment is not intended for production - there are no backups, there's a few quirks, but it could be used as-is and makes a good starting point. +This page attempts to explain how the parts fit together. + + +* [Pre-Requisites](#pre-requisites) +* [Files in deployment](#files-in-deployment) + * [`vars.tf`](#varstf) + * [`data.tf`](#datatf) + * [`ssl.tf`](#ssltf) + * [`server.tf`](#servertf) + + + + +## Pre-Requisites + +* You must already have a Liquid Web account. +* Your Liquid Web account or API user must be set to environment variables: + * `LWAPI_USERNAME` - your account username + * `LWAPI_PASSWORD` - your account username +* For the ACME TLS provider, your account must be set to + * `LIQUID_WEB_USERNAME` - your account username (for ACME) + * `LIQUID_WEB_PASSWORD` - your account username (for ACME) + * `LIQUID_WEB_URL` set to `"https://api.liquidweb.com"` + * `LIQUID_WEB_ZONE` set to your domain name +* You must have a DNS zone created for the domain you want to use +* You should create a `.tfvars` to change the domain - see below + +Once you have those prerequisites, deploy from this directory with: + +```bash +terraform init +terraform apply +``` + +Tear this down with: + +```bash +terraform destroy +``` + +## Files in deployment + +This example is made up of 4 files in an attempt to simplify the example. +Technically, this could be in one `.tf` file instead. +Below, the content in each file is explained. + +`output` sections in each file simply show things at the end. +They do not impact the deployment, just the output you see. +Relevant useful pieces are shown from each item. + +```hcl +output "instances" { + value = liquidweb_cloud_server.simple_server.*.ip +} +``` + +### `vars.tf` + +This is the first file, and likely the simplest, it just defines some default variables. +The two things that you likely only want to change are: + +```hcl +variable "site_name" { + type = string + default = "simple.example.com" +} + +variable "top_domain" { + type = string + default = "example.com" +} +``` + +* If you copy these files somewhere else, you can modify those values +* You can change them at cli `terraform apply -var "top_domain=example.com"` +* Environment variables - `export TF_VAR_TOP_DOMAIN="example.com"` +* We'd recommend using a [tfvars file](https://developer.hashicorp.com/terraform/language/values/variables#variable-definitions-tfvars-files) + +```hcl +top_domain = "example.com" +site_name = "wordpress.example.com" +``` + +### `data.tf` + +This file is focused on setting up the provider and a lot of pieces of data used elsewhere. + +First, the required providers are defined, that section is below. +This is used when you run `terraform init`. + +```hcl +terraform { + required_providers { + liquidweb = { + source = "liquidweb/liquidweb" + version = ">= 1.7.0" + } + acme = { + source = "vancluever/acme" + version = "2.17.1" + } + } +} +``` + +Next a bunch of random passwords and similar are generated: + +```hcl +resource "random_password" "server" { + length = 20 + special = false +} +``` + +Finally, templates are rendered. +Templates can be rendered inline, but here they are made into objects so terraform will detect changes. + +* `template` is a path to the location of the template (relative to the cwd) +* `vars` is a hash of variables available to be used in the template + +```hcl +data "template_file" "wp-config" { + template = file("${path.module}/templates/wp-config.php") + vars = { + dbhost = var.wordpress_dbhost + dbname = var.wordpress_dbname + dbuser = var.wordpress_dbuser + dbpass = random_password.wordpress_dbpass.result + salt = random_password.wordpress_salt.result + } +} +``` + +Finally, there are a few DNS records created for both the site and the server. +These are dependent on the server being created, and happen after that. +But `terraform` automatically creates the DNS records for ease of use. + +* `name` is where the DNS record is created +* `rdata` is what the DNS record should point to +* `zone` is the zone to create the DNS record in + +All of these are string fields, and can be changed to be whatever else is needed. + +```hcl +resource "liquidweb_network_dns_record" "server_dns" { + name = liquidweb_cloud_server.simple_server.domain + type = "A" + rdata = liquidweb_cloud_server.simple_server.ip + zone = var.top_domain +} +``` + +### `ssl.tf` + +`ssl.tf` incrementally creates the ssl, then outputs the domain name for the SSL. +With this method of SSL generation, you will need to run `terraform apply` to renew. + +Relevant links for this section: + +* [`acme_certificate` provider](https://registry.terraform.io/providers/vancluever/acme/latest/docs/resources/certificate#using-dns-challenges) +* [`liquidweb` challenge for use with zones hosted with LiquidWeb](https://registry.terraform.io/providers/vancluever/acme/latest/docs/guides/dns-providers-liquidweb) +* [source for the `terraform` provider (uses `lego`)](https://github.com/vancluever/terraform-provider-acme/tree/main/acme) +* [docs for `liquidweb` part within `lego`](https://go-acme.github.io/lego/dns/liquidweb/) +* [source for the `liquidweb` DNS challenge within `lego`](https://github.com/go-acme/lego/tree/master/providers/dns/liquidweb) +* [Let's Encrypt Endpoints](https://letsencrypt.org/docs/acme-protocol-updates/#api-endpoints) + +First, the Let's Encrypt URL is configured. + +```hcl +provider "acme" { + server_url = "https://acme-v02.api.letsencrypt.org/directory" +} + +Next a private key is created, and used to register with Let's Encrypt: +```hcl +resource "tls_private_key" "private_key" { + algorithm = "RSA" +} + +resource "acme_registration" "reg" { + account_key_pem = tls_private_key.private_key.private_key_pem + email_address = "nobody@${var.site_name}" +} +``` + +An SSL is requested, and the `liquidweb` DNS challenge is used. + +```hcl +resource "acme_certificate" "web_cert" { + account_key_pem = acme_registration.reg.account_key_pem + common_name = "${var.site_name}" + key_type = "4096" + +# subject_alternative_names = ["www2.example.com"] + + dns_challenge { + provider = "liquidweb" + } +} +``` + +This produces a `acme_certificate.web_cert` resource for use later. + +### `server.tf` + +The file `server.tf` contains one resource, but it's the most complex one - the server. +This resource depends on random values, templates, the TLS cert, and an SSH key. + +First, the server options are specified - this determines how the server is created. + +* `zone` (required) determines which zone to create the server in +* `config_id` (required) is the numeric id of the server type to create +* `template` (required) is the base image to use +* `domain` (required) is the server's hostname +* `password` (optional) will be the server's root passwordot provided +* `public_ssh_key` (optional) an SSH key inserted for root +* `lifecycle.create_before_desetroy` determines what to do when recreating a server` + +You need either `public_ssh_key` or `password`, but not both. +If no `password` is provide, a random one will be set. + +```hcl + zone = data.liquidweb_network_zone.zonec.network_zone_id + config_id = 1757 + template = "ROCKYLINUX_8_UNMANAGED" + domain = "wordpress-host${random_id.server.dec}.us-midwest-2.${var.top_domain}" + public_ssh_key = file("${path.root}/default.pub") + password = random_password.server.result + + lifecycle { + create_before_destroy = false + } +``` + +Next, the connection to that server is configured. +The SSH key at `default.pub` should be replaced with your pub key, then this should use that key. +If you would rather, you can use the root password or [see the docs](https://developer.hashicorp.com/terraform/language/resources/provisioners/connection). + +```hcl + connection { + type = "ssh" + user = "root" + agent = true + host = self.ip + } +``` + +Once the server is online and the connection is good, some commands are run: + +```hcl + provisioner "remote-exec" { + inline = [ + "yum install -y epel-release", + "yum install -y http://rpms.remirepo.net/enterprise/remi-release-8.rpm", + "yum install -y wget curl nginx mysql mysql-common mysql-server php82-php-fpm php82-php-mysqlnd php82-php-mbstring" + ] + } +``` + +Some files are also written to the vm. +All files here are from templates, but direct files could be done as well. +But see the [`file provisioner`](https://developer.hashicorp.com/terraform/language/resources/provisioners/file) docs and you can use a straight file instead as well. + +```hcl + provisioner "file" { + content = data.template_file.site-conf.rendered + destination = "/etc/nginx/conf.d/site.conf" +``` + +Once all of those are done, the server's up and ready to go through the Wordpress setup. diff --git a/examples/simple-wordpress/server.tf b/examples/simple-wordpress/server.tf new file mode 100644 index 0000000..4026ce5 --- /dev/null +++ b/examples/simple-wordpress/server.tf @@ -0,0 +1,79 @@ +resource "liquidweb_cloud_server" "simple_server" { + # count = 1 + + #config_id = "${data.liquidweb_storm_server_config.api.id}" + config_id = 1757 + zone = data.liquidweb_network_zone.zonec.network_zone_id + #data.liquidweb_network_zone.api.id + template = "ROCKYLINUX_8_UNMANAGED" + domain = "wordpress-host${random_id.server.dec}.us-midwest-2.${var.top_domain}" + public_ssh_key = file("${path.root}/default.pub") + password = random_password.server.result + + lifecycle { + create_before_destroy = false + } + + connection { + type = "ssh" + user = "root" + agent = true + host = self.ip + } + + provisioner "remote-exec" { + inline = [ + "yum install -y epel-release", + "yum install -y http://rpms.remirepo.net/enterprise/remi-release-8.rpm", + "yum install -y wget curl nginx mysql mysql-common mysql-server php82-php-fpm php82-php-mysqlnd php82-php-mbstring" + ] + } + + provisioner "file" { + content = "${acme_certificate.web_cert.certificate_pem}${acme_certificate.web_cert.issuer_pem}" + destination = "/etc/pki/tls/certs/${var.site_name}.crt" + } + provisioner "file" { + content = "${acme_certificate.web_cert.private_key_pem}" + destination = "/etc/pki/tls/private/${var.site_name}.key" + } + + provisioner "file" { + content = data.template_file.site-conf.rendered + destination = "/etc/nginx/conf.d/site.conf" + } + provisioner "file" { + content = data.template_file.php-conf.rendered + destination = "/etc/opt/remi/php82/php-fpm.d/site.conf" + } + provisioner "file" { + content = data.template_file.create-database.rendered + destination = "/root/create-database.sql" + } + provisioner "file" { + content = data.template_file.install-wordpress.rendered + destination = "/root/install-wordpress.sh" + } + provisioner "file" { + content = data.template_file.wp-config.rendered + destination = "/root/wp-config.php" + } + + provisioner "remote-exec" { + inline = [ + "systemctl start mysqld.service", + "chmod +x /root/install-wordpress.sh", + "/root/install-wordpress.sh", + "mysql < /root/create-database.sql", + "systemctl enable nginx.service php82-php-fpm.service mysqld.service", + "systemctl start nginx.service php82-php-fpm.service mysqld.service", + "firewall-cmd --zone public --permanent --add-port 80/tcp", + "firewall-cmd --zone public --permanent --add-port 443/tcp", + "firewall-cmd --reload" + ] + } +} + +output "instances" { + value = liquidweb_cloud_server.simple_server.*.ip +} diff --git a/examples/simple-wordpress/ssl.tf b/examples/simple-wordpress/ssl.tf new file mode 100644 index 0000000..5d8e396 --- /dev/null +++ b/examples/simple-wordpress/ssl.tf @@ -0,0 +1,27 @@ +provider "acme" { + server_url = "https://acme-v02.api.letsencrypt.org/directory" +} + +resource "tls_private_key" "private_key" { + algorithm = "RSA" +} + +resource "acme_registration" "reg" { + account_key_pem = tls_private_key.private_key.private_key_pem + email_address = "nobody@${var.site_name}" +} + +resource "acme_certificate" "web_cert" { + account_key_pem = acme_registration.reg.account_key_pem + common_name = "${var.site_name}" + key_type = "4096" + # subject_alternative_names = ["www2.example.com"] + + dns_challenge { + provider = "liquidweb" + } +} + +output "ssl" { + value = acme_certificate.web_cert.certificate_domain +} \ No newline at end of file diff --git a/examples/simple-wordpress/templates/create-database.sql b/examples/simple-wordpress/templates/create-database.sql new file mode 100644 index 0000000..9f99aed --- /dev/null +++ b/examples/simple-wordpress/templates/create-database.sql @@ -0,0 +1,3 @@ +CREATE DATABASE ${dbname} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +CREATE USER '${dbuser}'@'${dbhost}' IDENTIFIED BY '${dbpass}'; +GRANT ALL PRIVILEGES ON ${dbname}.* TO '${dbuser}'@'${dbhost}'; \ No newline at end of file diff --git a/examples/simple-wordpress/templates/install-wordpress.sh b/examples/simple-wordpress/templates/install-wordpress.sh new file mode 100755 index 0000000..e735b8a --- /dev/null +++ b/examples/simple-wordpress/templates/install-wordpress.sh @@ -0,0 +1,12 @@ +groupadd ${user} +useradd -g ${user} ${user} + +mkdir -p /home/${user}/www +chmod g+x /home/${user} +usermod nginx -G wordpress + +curl --silent https://wordpress.org/latest.tar.gz|tar -C /home/${user}/www --strip-components=1 -xz 'wordpress/*' + +mv /root/wp-config.php /home/${user}/www/ + +chown -R wordpress. /home/wordpress/www \ No newline at end of file diff --git a/examples/simple-wordpress/templates/nginx.conf b/examples/simple-wordpress/templates/nginx.conf new file mode 100644 index 0000000..b2f408a --- /dev/null +++ b/examples/simple-wordpress/templates/nginx.conf @@ -0,0 +1,41 @@ + +server { + listen 80; + listen 443 ssl; + server_name ${domain}; + ssl_certificate /etc/pki/tls/certs/${domain}.crt; + ssl_certificate_key /etc/pki/tls/private/${domain}.key; + + root /home/${user}/www; + index index.php index.html index.htm; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + client_max_body_size 500M; + + location / { + try_files $uri $uri/ /index.php?$args; + } + + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { + expires max; + log_not_found off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + + location ~ \.php$ { + fastcgi_pass unix:/var/opt/remi/php82/run/php-fpm/${user}.sock; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } +} \ No newline at end of file diff --git a/examples/simple-wordpress/templates/php-fpm.conf b/examples/simple-wordpress/templates/php-fpm.conf new file mode 100644 index 0000000..867350d --- /dev/null +++ b/examples/simple-wordpress/templates/php-fpm.conf @@ -0,0 +1,15 @@ +[${user}] +pm = ondemand +pm.max_children = 30 +pm.start_servers = 2 +pm.min_spare_servers = 2 +pm.max_spare_servers = 10 +pm.process_idle_timeout = 10s + +user = ${user} +group = ${user} + +listen = /var/opt/remi/php82/run/php-fpm/wordpress.sock +listen.owner = nginx +listen.group = nginx +listen.mode = 0660 diff --git a/examples/simple-wordpress/templates/wp-config.php b/examples/simple-wordpress/templates/wp-config.php new file mode 100644 index 0000000..5a7159e --- /dev/null +++ b/examples/simple-wordpress/templates/wp-config.php @@ -0,0 +1,96 @@ +