Skip to content

OCI – 04 – Build Servers

Now that we have a network in our tenancy, we can start building servers to some fun software. We will continue to use Terraform and our same repository for deploying and configuring the servers. If you want to catch up, the repository and branch for the last post is here.

I’ll be running the Terraform build from the OCI Cloud Shell, but this will work from your local terminal too.

The first thing we need to do is specify that we will be using two providers as part of our project. The oci provider is already pre-installed in OCI Cloud Shell, but to be clear that our project is using it we’ll include oci in the provider list. At the time of writing this post, version 5.18 is the latest release. We will also include the tls provider. The tlsprovider allows us to create different keys and use them inside our code. We will use this to create an SSH key for our servers and use that connect after they are built.

Update provider.tf so the terraform{} section looks like this.

terraform {
  backend "s3" {}
  required_providers {
    oci = {
      source = "oracle/oci"
      version = "5.18.0"
    }
		tls = {
	    source = "hashicorp/tls"
	    version = "4.0.4"
	  }
  }
}

Because we are adding providers, we need to re-initialize our Terraform project. Remove previous lock file and re-run the init command.

$ rm .terraform.lock.hcl
$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding oracle/oci versions matching "5.18.0"...
- Finding hashicorp/tls versions matching "4.0.4"...
- Installing oracle/oci v5.18.0...
- Installed oracle/oci v5.18.0 (signed by a HashiCorp partner, key ID 1533A49284137CEB)
- Installing hashicorp/tls v4.0.4...
- Installed hashicorp/tls v4.0.4 (signed by HashiCorp)

Terraform has been successfully initialized!

Create defined tags for our future servers. We’ll use tags to reference servers rather than use specific server names. Add a Tag Namespace and a tag called “Role” with pre-defined values to the compartment.tf file.

resource "oci_identity_tag_namespace" "minecraft_tags_ns" {
    compartment_id = oci_identity_compartment.minecraft.id
    description = "Minecraft Tags"
    name = "minecraft"
}

resource "oci_identity_tag" "role" {
    description = "Server Role"
    name = "role"
    tag_namespace_id = oci_identity_tag_namespace.minecraft_tags_ns.id
    validator {
        validator_type = "ENUM"
        values = ["loadbalancer", "minecraft"]
    }
}

Next, create servers.tf to build a bastion server that will run in our DMZ. (There is a Bastion OCI service, but for our demo we’ll be using this DMZ server for both SSH access and to run HAProxy as a load balancer. Yes, I know there is an OCI Load Balancer service too.)

Servers Resources

The first server resource we will create is a key pair to use for SSH connections to our servers. These will be used by Terraform for provisioning, and for our initial testing with the server.

resource "tls_private_key" "public_private_key_pair" {
  algorithm   = "RSA"
}

output "private_ssh_key" {
    value = tls_private_key.public_private_key_pair.private_key_openssh
    sensitive = true
}

We will define some variables with defaults so that we can change the size of our server later on.

variable "shape"     { default="VM.Standard.E4.Flex" }
variable "burstable" { default="BASELINE_1_2" }
variable "cpus"      { default="1" }
variable "memory"    { default="8" }

We also need to select a base Linux image that our server will run on. We could use a variable for the image’s OCID, but if we build a server 6 months later our data source will grab the latest patched image. The data source will grab all of the Oracle Linux 8 images for our region, sort them by release date. We can then reference the first result later on when we create our instance to get the most recent OCID.

data "oci_core_images" "linux_images" {
    compartment_id           = oci_identity_compartment.minecraft.id
    operating_system         = "Oracle Linux"
    operating_system_version = "8"
    sort_by                  = "TIMECREATED"
    sort_order               = "DESC"
}

Bastion Server

Now for the fun part, defining our server. Add this oci_core_instance to servers.tf .

resource "oci_core_instance" "bastion" {
    compartment_id      = oci_identity_compartment.minecraft.id
    availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
    display_name        = "Minecraft Bastion"

    shape = var.shape
    shape_config {
        memory_in_gbs = var.memory
        ocpus         = var.cpus
        baseline_ocpu_utilization = var.burstable
    }

    create_vnic_details {
        subnet_id        = oci_core_subnet.dmz.id
        assign_public_ip = true
        hostname_label   = "bastion"
        # nsg_ids          = [oci_core_network_security_group_security_rule.minecraft_from_public.id]
    }

    metadata = {
      ssh_authorized_keys = tls_private_key.public_private_key_pair.public_key_openssh
    }

    source_details {
        # <https://docs.oracle.com/en-us/iaas/images/image/e1a40b7b-4d0b-461f-9078-0d2da7283349>
        source_id   = data.oci_core_images.linux_images.images[0].id
        source_type = "image"
    }

    lifecycle {
      ignore_changes = [source_details, metadata]
    }

		defined_tags = { "minecraft.role" = "loadbalancer" }
}

There is a lot going on with our server definition so let’s break it down.

  1. Compartment and AD: we tell OCI which compartment and what Availability Domain we want our server created in.
  2. Shape: the shape includes the hardware to use for our server, as well as how many OCPUs, memory, and a burstable settting. We set our baseline_ocpu_utilization to 50%, which means our CPU will have half the normal performance unless we run processes on it that require more. It’s designed for servers, like our bastion instance, that are mostly idle and only occasionally need more CPU. It’s a great way to save money.
  3. VNIC Details: we configure the network connection for the instance here including which subnet it belongs to, a network label (dns name), and any Network Security Groups that should be assigned.
  4. Metadata: we can configure SSH authentication here as well as pass in any cloud-init configuration. We are setting our generated SSH public key and it will be assigned to the opc user.
  5. Source Details: we define the base platform image here using our data source.
  6. Lifecycle: this is a meta block for Terraform. We tell Terraform to ignore any changes in the Lifecycle Details blocks. We do this because we don’t want Terraform to try and rebuild the server every time there is a new Oracle Linux 8 image.

Last, we want to output the public IP address for our bastion server so we know where to SSH to.

output "bastion_public_ip" {
    value = oci_core_instance.bastion.public_ip
}

This is the current servers.tf file.

variable "shape"     { default="VM.Standard.E4.Flex" }
variable "burstable" { default="BASELINE_1_2" }
variable "cpus"      { default="1" }
variable "memory"    { default="8" }

data "oci_core_images" "linux_images" {
    compartment_id           = oci_identity_compartment.minecraft.id
    operating_system         = "Oracle Linux"
    operating_system_version = "8"
    sort_by                  = "TIMECREATED"
    sort_order               = "DESC"
}

resource "tls_private_key" "public_private_key_pair" {
  algorithm   = "RSA"
}

resource "oci_core_instance" "bastion" {
    compartment_id      = oci_identity_compartment.minecraft.id
    availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
    display_name        = "Minecraft Bastion"

    shape = var.shape
    shape_config {
        memory_in_gbs = var.memory
        ocpus         = var.cpus
        baseline_ocpu_utilization = var.burstable
    }

    create_vnic_details {
        subnet_id        = oci_core_subnet.dmz.id
        assign_public_ip = true
        hostname_label   = "bastion"
        nsg_ids          = [oci_core_network_security_group.minecraft_dmz.id]
    }

    metadata = {
      ssh_authorized_keys = tls_private_key.public_private_key_pair.public_key_openssh
    }

    source_details {
        # <https://docs.oracle.com/en-us/iaas/images/image/e1a40b7b-4d0b-461f-9078-0d2da7283349>
        source_id   = data.oci_core_images.linux_images.images[0].id
        source_type = "image"
    }

    lifecycle {
      ignore_changes = [source_details, metadata]
    }

		defined_tags = { "minecraft.role" = "minecraft" }
}

output "bastion_public_ip" {
    value = oci_core_instance.bastion.public_ip
}

output "private_ssh_key" {
    value = tls_private_key.public_private_key_pair.private_key_openssh
    sensitive = true
}

Let’s create our first server in OCI. Start with a terraform plan to verify that our two new resources are picked up.

$ terraform plan

Terraform will perform the following actions:

	# oci_identity_tag_namespace.minecraft_tags_ns will be created
  + resource "oci_identity_tag_namespace" "minecraft_tags_ns" {
...
  # oci_identity_tag.role will be created
  + resource "oci_identity_tag" "role" {
...
  # oci_core_instance.bastion will be created
  + resource "oci_core_instance" "bastion" {
...
  # tls_private_key.public_private_key_pair will be created
  + resource "tls_private_key" "public_private_key_pair" {
...
Plan: 4 to add, 0 to change, 0 to destroy.

Now let’s apply the plan and create the resources.

$ terraform apply

Plan: 4 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

tls_private_key.public_private_key_pair: Creating...
tls_private_key.public_private_key_pair: Creation complete after 0s [id=76fd9454d68c9ca3cbd196170ab8075b198b3c69]
...
oci_core_instance.bastion: Creating...
oci_core_instance.bastion: Still creating... [10s elapsed]
oci_core_instance.bastion: Creation complete after 34s [id=ocid1.instance.oc1.us-chicago-1.xxxx]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

ads = tolist([...]])
bastion_public_ip = "xxx.xxx.xxx.xxx"
private_ssh_key = <sensitive>

After we create our resources, we can save the private key to file and use it for our SSH connections

$ mkdir -p ~/.ssh
$ terraform output -raw private_ssh_key | tee ~/.ssh/id_rsa_bastion
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdz
...
-----END OPENSSH PRIVATE KEY-----

$ chmod 600 ~/.ssh/id_rsa_bastion

Test your SSH connection into the new bastion server.

$ ssh -i ~/.ssh/id_rsa_bastion opc@xxx.xxx.xxx.xxx
Are you sure you want to continue connecting (yes/no)? yes

[opc@bastion ~]$ hostname -f
bastion.dmz.minecraft.oraclevcn.com

Success! We created a server in our DMZ and can access it via SSH.

Next up, building our server to run Minecraft.

Minecraft Server

First, add two more variables to set different CPU and Memory values for the Minecraft server.

variable "cpus_minecraft"      { default="1" }
variable "memory_minecraft"    { default="12" }

Create another oci_core_instance resource for the Minecraft server.

resource "oci_core_instance" "minecraft" {
    compartment_id      = oci_identity_compartment.minecraft.id
    availability_domain = data.oci_identity_availability_domains.ads.availability_domains[0].name
    display_name        = "Minecraft Server"

    shape = var.shape
    shape_config {
        memory_in_gbs = var.memory
        ocpus         = var.cpus
        baseline_ocpu_utilization = var.burstable
    }

    create_vnic_details {
        subnet_id        = oci_core_subnet.apps.id
        hostname_label   = "minecraft"
				assign_public_ip = false
        nsg_ids          = [oci_core_network_security_group.minecraft_app.id]
    }

    metadata = {
      ssh_authorized_keys = tls_private_key.public_private_key_pair.public_key_openssh
    }

    source_details {
        # <https://docs.oracle.com/en-us/iaas/images/image/e1a40b7b-4d0b-461f-9078-0d2da7283349>
        source_id   = data.oci_core_images.linux_images.images[0].id
        source_type = "image"
    }

    lifecycle {
      ignore_changes = [source_details, metadata]
    }
}

Besides the name changes in this resource, we also adjusted the subnet_id, changed the assign_public_ip flag, and the nsg_ids assigned to the VNIC. The rest of our definition is the same as the bastion resource.

Let’s build our Minecraft instance but this time we will save the plan to re-use when we apply it.

$ echo '*.tfplan' | tee -a .gitignore
$ terraform plan -out=serverbuild.tfplan

Terraform will perform the following actions:

  # oci_core_instance.minecraft will be created
  + resource "oci_core_instance" "minecraft" {
...
Plan: 1 to add, 0 to change, 0 to destroy.

Saved the plan to: serverbuild.tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "serverbuild.tfplan"

We can now reference the serverbuild.tfplan instead of replanning when it’s time to apply.

$ terraform apply "serverbuild.tfplan"

oci_core_instance.minecraft: Creating...
oci_core_instance.minecraft: Still creating... [10s elapsed]
oci_core_instance.minecraft: Creation complete after 34s [id=ocid1.instance.oc1.us-chicago-1.xxxx]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Because the “minecraft” instance is in our VCN and in a private subnet, we need to use our bastion server as a jump host to connect. We will setup SSH Agent in our shell to help manage passing the SSH keys to our bastion server.

$ echo 'eval $(ssh-agent)' | tee -a ~/.bashrc
$ source ~/.bashrc
$ ssh-add ~/.ssh/id_rsa_bastion 
Identity added: /home/dan/.ssh/id_rsa_bastion

Enable SSH Forwarding on our Bastion server.

$ ssh -A opc@<bastion_public_ip>

[opc@bastion ~]$ exit

Now we can SSH jump to our Minecraft server via the Bastion server.

$ ssh -J opc@<bastion_public_ip> opc@minecraft.apps.minecraft.oraclevcn.com
Are you sure you want to continue connecting (yes/no)? yes

[opc@minecraft ~]$ hostname -f
minecraft.apps.minecraft.oraclevcn.com

Cool! We have two servers built and we can connect to both of them. The final code for this post on Github here. Next up is how we install and configure our new servers using Terraform and Ansible.

Leave a Reply

Your email address will not be published. Required fields are marked *