Creating a DigitalOcean Droplet with Terraform - Part 3 of 3 - Cloud-init
Tags: terraform, digitalocean, devops
Published: 2020-04-28 13:46:13

---
title: Creating a DigitalOcean Droplet with Terraform - Part 3 of 3 - Cloud-init
published: true
date: 2020-04-28 00:57:06 UTC
tags: Terraform, DigitalOcean, DevOps
canonical_url: 
---

 ![Creating a DigitalOcean Droplet with Terraform - Part 3 of 3 - Cloud-init](https://images.unsplash.com/photo-1558494949-ef010cbdcc31?ixlib=rb-1.2.1&q=80&fm=jpg&crop=entropy&cs=tinysrgb&w=2000&fit=max&ixid=eyJhcHBfaWQiOjExNzczfQ)

In parts [1 and 2 of our Creating a DigitalOcean Droplet with Terraform](https://dev.to/bitleaf_io/creating-a-digitalocean-droplet-with-terraform-part-1-of-3-1pko) series we setup our Terraform configuration and created a DigitalOcean droplet and volume. In the final part we now are going to configure that droplet so when it gets created it already has the OS setup how we want it.

To be able to setup the droplet operating system as part of our Terraform configuration we are going to use the cloud-init method. There are different ways to go about this, but cloud-init is the standard to be able to setup your cloud based instances in an automated fashion. There is a lot you can do with it and there are some [examples of what cloud-init can do](https://cloudinit.readthedocs.io/en/latest/topics/examples.html) on the [cloud-init site](https://cloudinit.readthedocs.io/en/latest/index.html).

Cloud-init is just another configuration file that we can call from our Terraform configuration. Cloud-init uses the YAML format. So when working with cloud-init files, make sure to watch your indentations.

So let's take a look at the cloud-init file and then we'll go through what it's doing in our example.

```
#cloud-config

package_update: true
package_upgrade: true
package_reboot_if_required: true

groups:
    - docker

users:
    - name: leaf
      lock_passwd: true
      shell: /bin/bash
      ssh_authorized_keys:
      - ${init_ssh_public_key}
      groups: docker
      sudo: ALL=(ALL) NOPASSWD:ALL

packages:
  - apt-transport-https
  - ca-certificates
  - curl
  - gnupg-agent
  - software-properties-common
  - unattended-upgrades

runcmd:
  - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
  - add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
  - apt-get update -y
  - apt-get install -y docker-ce docker-ce-cli containerd.io
  - systemctl start docker
  - systemctl enable docker
  - curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  - chmod +x /usr/local/bin/docker-compose

```

We start off with the line...

```
#cloud-config

```

This line is critical in telling cloud-init that we are using a cloud-config style file. You can also do a standard bash shell indication.

Our next three lines...

```
package_update: true
package_upgrade: true
package_reboot_if_required: true

```

These simply tell our operating system to update the packages to the latest version and do any necessary reboots. This is great and saves you the manual effort.

Now for the user/group information...

```
groups:
    - docker

users:
    - name: leaf
      lock_passwd: true
      shell: /bin/bash
      ssh_authorized_keys:
      - ${init_ssh_public_key}
      groups: docker
      sudo: ALL=(ALL) NOPASSWD:ALL

```

Now we are going to tell our operating system to create a new group 'docker', and a new user 'leaf'. For the user we set:

- name: leaf ... This sets the username to 'leaf'
- lock\_passwd: true ... This turns off password logins
- ssh\_authorized\_keys ... This is a variable that contains our DigitalOcean SSH key. This will let you login as 'leaf' using your existing SSH key. The `${init_ssh_public_key}` is going to be set when we add the cloud-init call to our Terraform configuration.
- groups: docker ... Assign the user to the 'docker' group
- sudo: ALL=(ALL) NOPASSWD:ALL ... This will add the user to the Sudoers file

Now for the packages...

```
packages:
  - apt-transport-https
  - ca-certificates
  - curl
  - gnupg-agent
  - software-properties-common
  - unattended-upgrades

```

Most of this is just setting up basic package management to make it more secure and easier for package updates. The last one 'unattended-upgrades' is fantastic for your cloud servers. This will automatically install all security related updates to your server so you don't have to keep logging in and patching them (at least for security patches). You can of course also add your own packages to the list.

The final piece does our docker and docker-compose installs...

```
runcmd:
  - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
  - add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
  - apt-get update -y
  - apt-get install -y docker-ce docker-ce-cli containerd.io
  - systemctl start docker
  - systemctl enable docker
  - curl -L "https://github.com/docker/compose/releases/download/1.25.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
  - chmod +x /usr/local/bin/docker-compose

```

These commands are simply a way to install docker and docker-compose on Ubuntu. There is nothing changed from if you needed to run this manually. You just have each step as a separate yaml entry.

So that's it for our cloud-init example. Now let's go see how to have that run as part of our Terraform setup

* * *

Back in Terraform land we need to update our configuration to call our cloud-init.yaml file.

Here is our updated 'droplet\_volume.tf' file with our cloud-init pieces included. I'll highlight and discuss those pieces below.

```
# Specify the Terraform provider to use
provider "digitalocean" {
  token = var.do_token
}

data "template_file" "cloud-init-yaml" {
  template = file("${path.module}/files/cloud-init.yaml")
  vars = {
    init_ssh_public_key = file(var.ssh_public_key)
  }
}

# Setup a DO volume
resource "digitalocean_volume" "bitleaf_volume_1" {
  region = "nyc3"
  name = "biteaf-volume-1"
  size = 5
  initial_filesystem_type = "ext4"
  description = "bitleaf volume 1"
}

# Setup a second DO volume
resource "digitalocean_volume" "bitleaf_volume_2" {
  region = "nyc3"
  name = "biteaf-volume-2"
  size = 5
  initial_filesystem_type = "ext4"
  description = "bitleaf volume 2"
}

# Setup a DO droplet
resource "digitalocean_droplet" "bitleaf_server_1" {
  image = var.droplet_image
  name = "bitleaf-server-1"
  region = var.region
  size = var.droplet_size
  private_networking = var.private_networking
  ssh_keys = [
    var.ssh_key_fingerprint
  ]
  user_data = data.template_file.cloud-init-yaml.rendered
}

# Connect the volume to the droplet
resource "digitalocean_volume_attachment" "bitleaf_volume_1" {
  droplet_id = digitalocean_droplet.bitleaf_server_1.id
  volume_id = digitalocean_volume.bitleaf_volume_1.id
}

# Connect the second volume to the droplet
resource "digitalocean_volume_attachment" "bitleaf_volume_2" {
  droplet_id = digitalocean_droplet.bitleaf_server_1.id
  volume_id = digitalocean_volume.bitleaf_volume_2.id
}

# Output the public IP address of the new droplet
 output "public_ip_server" {
  value = digitalocean_droplet.bitleaf_server_1.ipv4_address
}

```

Most of our Terraform configuration is the same. We did add one new block.

```
data "template_file" "cloud-init-yaml" {
  template = file("${path.module}/files/cloud-init.yaml")
  vars = {
    init_ssh_public_key = file(var.ssh_public_key)
  }
}

```

This is a data block. In my setup I have my 'cloud-init.yaml' file in a 'files' directory. So the `template` parameter is just reading that file in. The reason it's called a 'template' parameter is because it allows us to replace variable entries in our cloud-init file. In our case we wanted to put in our ssh key. In the `vars` parameter we are setting the `init_ssh_public_key` variable with our local public key. The file means it's reading the contents of the file that is in the path that we specified in our 'variables.tf' file for `ssh_public_key`.

The other thing we added to our Terraform configuration is

```
user_data = data.template_file.cloud-init-yaml.rendered

```

under the Droplet resource block. This is populating the `user_data` DigitalOcean property with the contents of our rendered 'cloud-init.yaml' file. By _rendered_ it just means that the template was loaded and any variable substitutions have been made.

That's it. Just those couple of changes to our Terraform configuration and we'll have a nicely customized Droplet ready to go.

I found the best way to learn what you can do with cloud-init is to check out their [cloud-init examples page](https://cloudinit.readthedocs.io/en/latest/topics/examples.html). The great thing about cloud-init is that it is a standard that is followed in many cloud providers. This isn't anything specific to one provider, so your knowledge will be portable.


<!--kg-card-end: markdown-->