Terraform and UpCloud Part 2

This article is part 0 in a series: Server setup on 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 servers and how to tag servers.

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

resource "upcloud_server" "webserver" {
  count    = 1
  zone     = "de-fra1"
  hostname = "super-duper-webserver"
  plan     = "1xCPU-1GB"
  ipv6     = false
  
  login {
    user = "my-name"
    keys = [
        "my-public-ssh-key"
    ]

    create_password = false
  }

  storage_devices = [
    {
        tier    = "maxiops"
        size    = 25
        action  = "clone"
        storage = "Ubuntu Server 16.04 LTS (Xenial Xerus)"
    }
  ]
}

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 IP address, 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.

Note: 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, for now, we will use the firewall that UpCloud provides through their service.

The server in this example is a webserver, 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 trafic.

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" "http_ssh" {
    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" "https_ssh" {
    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 totally 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 webserver with the following two tags: ubuntu, web.

As of writing, there is a bug in the UpCloud provider which does not allow to create tags, but there is a pullrequest waiting to fix it and I will for now use the configuration that the pullrequest uses, if the pullrequest is denied or something else is added, I will change the example.

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

Working with terraform is easy, as long as you know what you are doing, as I previously stated, the documentation of the provider we use is currently quite slim, mainly due to it being a work in progress, but hopefully this series of posts will be able to make it a bit easier to know what to do and what is available.

In the next part of the series, we will dig in to Ansible to be able to install software on the server, it would be possible to do this directly in the terraform configurations, but ansible is a tool which is a lot easier to use for that type of configuration.

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: