OCI – 04 – Build Servers
Dec 05, 2023Dan Iverson
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 tls
provider 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.
- Compartment and AD: we tell OCI which compartment and what Availability Domain we want our server created in.
- 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. - 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.
- 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. - Source Details: we define the base platform image here using our data source.
- 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 [email protected]
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> [email protected]
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.
Note: This was originally posted by Dan Iverson and has been transferred from a previous platform. There may be missing comments, style issues, and possibly broken links. If you have questions or comments, please contact [email protected].