Terraform and UpCloud Part 2

This article is part 0 in a series: Terraform, Ansible and UpCloud



So far, we have gone through the basics of provisioning a machine on UpCloud, but there is more that is possible to do with terraform and the UpCloud API, in this post I intend to go through how to create firewall rules (using the firewall provided by UpCloud), how to change and tag servers.

The following is the initial provisioned server, using terraform HCL2 (HashiCorp Configuration Language)

resource "upcloud_server" "my_first_resource" {
  count = 1
  zone  = "de-fra1"
  hostname = "my-first-resource"
  plan = "1xCPU-1GB" 

  storage_devices {
    tier    = "maxiops"
    size    = 25
    action  = "clone"
    storage = "Ubuntu Server 18.04 LTS (Bionic Beaver)"
  }
  

  login {
    user = "my-name"
    keys = [
        "my-public-ssh-key"
    ]
    create_password = false
  }
}

Changing specs

Terraform is quite a clever tool. The state of the servers are fetched from the API, so that terraform can check if a given configuration matches the current running servers or not. If it does not, terraform will be able to update resources or even remove them if one is removed from the configuration.
Some of the configurations will force re-create the resource, but the server configurations does not.
When you add or remove a tag, or you change the storage size or change cpu and memory, the server will likely have to be “rebooted”, a VPS reboot quite quick, so It’s not that big of a deal, but if you are looking for a 100% uptime cluster, you might want to provision a new server and then remove the old one when the new is running.
There is not much to show in form of code or configuration when it comes to changing the server, so I’ll just stick to text here.

The local terraform cache can be added to your VCS or similar to be shared with other users, with a clean cache, terraform will not be able to find the servers because of the id not being stored and it is the only way to identify a unique server.

Adding firewall rules

Firewalls are quite important. I often use Ubuntu on my servers, and Ubuntu can use the UFW (uncomplicated firewall) package. UFW requires some configuration and if we where to set it up on the servers we would have to run commands on the server. We will get to that kind of stuff in a later post, but for now we will use the firewall that UpCloud provides through their service.

The server in this example is a web server, and all it need to have exposed to the public net is its web ports and possibly the ssh port if one wish to be able to connect to it.
We will here set up four rules, firstly, the ssh rule, then the ports and at the end, a default rule for incoming traffic, one to block all traffic on all ports that is from the outside. We keep the outgoing ports open (for now).

When creating a firewall rule with terraform, we will create a new resource for each rule. If a rule is removed from the configuration at a later point, it will be removed, if it changes, it will be changed. So each rule is its own resource.
Worth mentioning too is that for terraform to be able to identify a given rule when running remove or update, it have to use the position parameter. Hence the position is required using terraform, but optional when using the API.

resource "upcloud_firewall_rule" "fw_ssh" { # As you might remember, the identifier and provider
                                            # have to have a unique name when combined.
    # The 'depends_on' parameter makes sure that the defined resource exists before this resource is created.
    depends_on = ["upcloud_server.webserver"]
    # What we do here is use the webserver id, the scripts are ran together
    # so it's possible for one resource to reference another, as long as it already exists.
    server_id = upcloud_server.webserver.id
    direction = "in" # This rule applies to incoming connections only.
    action    = "accept"

    destination_port_start = "22"
    destination_port_end   = "22"

    family   = "IPv4" # In this case, we only bother with ipv4 as we have disabled ipv6.
    comment  = "SSH Port rule." # Comment is not required, but it's always nice to have.
    position = "1" # This places the rule at the first position in the rule list.
}

There we go. When we run terraform apply next time, we will create a firewall rule for the server which will allow for incoming SSH traffic.

The next two are quite similar to the above, both will allow for incoming, and the only real difference is the ports:

resource "upcloud_firewall_rule" "fw_http" {
    depends_on = ["upcloud_server.webserver"]

    server_id = upcloud_server.webserver.id
    direction = "in"
    action    = "accept"
    position  = "2"
    family    = "IPv4"

    destination_port_start = "80"
    destination_port_end   = "80"
}

resource "upcloud_firewall_rule" "fw_https" {
    depends_on = ["upcloud_server.webserver"]

    server_id = upcloud_server.webserver.id
    direction = "in"
    action    = "accept"
    position  = "3"
    family    = "IPv4"

    destination_port_start = "443"
    destination_port_end   = "443"
}

Now we have the rules that allows for connecting to the server via http, https and ssh. That’s great, but what good is rules if all ports are exposed in either case?

Creating a default rule is even easier than one for a specific port, the big difference is that it have no ports specified.
In this case our default rule is a “drop” rule, but it could just as well have been a “allow” rule if we wanted.

resource "upcloud_firewall_rule" "fw-default-in" {
  depends_on = ["upcloud_server.webserver"]

  server_id = upcloud_server.webserver.id
  direction = "in"
  action    = "drop"
  family    = "IPv4"
  position  = "4"
}

There we go! All rules set up for ONLY allowing connection via http, https and ssh. If you apply the changes, you should be able to navigate to the servers firewall rules in the web-interface of UpCloud and see all the rules in a neat list. Don’t change them manually, you should from now on only do changes through the terraform configurations!

Tagging servers

Tagging a server can be a nice thing to do, I love having some metadata on stuff, so that it’s easier to know what’s on them and being able to show only stuff with specific tags.
In this example we will tag the web server with the following two tags: ubuntu, web.

As with everything else so far, a Tag is also a resource, the provider exposes it through the upcloud_tag resource. Each resource should have a name and a list of servers which the tag should be applied to, so if there is more than one server that should use the same tag, it can be added to the servers list.

resource "upcloud_tag" "ubuntu" { # The identifier we use for the ubuntu tag is just ubuntu, 
                                  # no need to make it more complicated!
    depends_on = ["upcloud_server.webserver"] # As with the firewall rules, we want this to be applied AFTER the server exists.

    name    = "ubuntu" # Tag name, same as the identifier.
    servers = [ # The servers list is a list with server IDs as strings
        upcloud_server.webserver.id
    ]
}

resource "upcloud_tag" "web" {
    depends_on = ["upcloud_server.webserver"]
    name       = "web"
    servers    = [
        upcloud_server.webserver.id
    ]
}

When we now run terraform apply we will have two tags on our server!

Final words

Terraform is quite straight forward and easy to both use and read. It have some limitations but most of it is possible to get around in one way or another.
The UpCloud provider is not very well documented, but if you look at its code (go) and the API docs, you should be able to figure out what resource types there are and what the parameters that they take are.

Next part of the series we will dig in to Ansible and use it to install software on the servers. It is possible to do this through terraform too, but I find ansible a bit better suiting for software while I really like using terraform for the server provisioning.

As always, if you find anything that you think is wrong, have any critique, questions or just want to say something, don’t hesitate to post a comment below!

Updated: