Avoid rework
I hope that if you learned anything from my earlier post, Why Infrastructure as Code Matters, it was that if you think you’ll do something more than once, you should script it; and if you can script it, you should create a stack to automate it.
This post will describe a simple example of that truism. We will create an always-free compute instance from an OCI platform image, install Docker and Docker Compose, and run a simple distributed tracing example with Jaeger and its demo application. For more information on Jaeger and monitoring for microservices, check out Distributed Tracing in Practice.
Check out the sample code here: orm-docker-compute-stack
OCI platform images
OCI platform images do not include much beyond the operating system. One exception to that is the Oracle Linux Cloud Developer image which includes development tools, languages and Oracle Cloud Infrastructure software development kits (SDKs) to rapidly launch a comprehensive development environment. For our always-free instance we will rely on two features that are included in all platform images.
- Installing software: Oracle Linux and CentOS images are preconfigured to let you install and update packages from the repositories on the Oracle public yum server.
- User data: All platform images give you the ability to run custom scripts or supply custom metadata when the instance launches using cloud-init.
Installing software in your cloud compute instances by hand can be cumbersome and error prone. You need to have a private key, ssh into the instance and run each of your commands. Let’s avoid those pitfalls by taking the lessons of infrastructure as code to heart and use an Oracle Resource Manager Terraform stack to create and bootstrap our instance with a cloud-init script.
Start with containers
Containers are lightweight and contain everything needed to run your applications. Containers simplify development and delivery of distributed applications and are the de-facto method for deploying cloud-native apps. Potentially the only thing you ever need to install in your instance is something that manages containers. For that reason I’ll use both Docker and Docker Compose as the example software to install on our compute instance.
Docker’s core value proposition is based on containers and how developers and teams can use them to write, collaborate on and deploy applications without worrying about the particularities of different cloud or local development environments. Docker is an open source platform that enables developers to build, deploy, run, update and manage containers.
Docker Compose is a command-line tool that uses a specially formatted YAML file as input to assemble and run single, or multiple, containers as applications. This allows developers to develop, test, and deliver a single file describing their application, and use only one command to start and stop it reliably.
If you’d like to look further afield than Docker, here’s some other options:
- Containerd is a high-level container runtime that runs
runcunder the hood to provide an interface between the OS and container engines. Runc is a daemon with Windows and Linux support that abstracts OS-specific functionality and makes it easier to run and supervise containers and manage image transfer and storage. - Lxd is another open-source container engine that is designed specifically for LXC Linux Containers.
- Podman is a popular daemon-less, open-source, Linux-native container engine developed by RedHat, that is used to build, run and manage Linux OCI containers and container images. Although Podman provides a command-line interface similar to Docker’s, it operates differently.
If you’re interested in Podman but also like Docker Compose, check out this tutorial on Using Compose Files with Podman.
Implementing Infrastructure as Code
Here’s our concrete implementation that illustrates the Infrastructure as Code principles. We will use Oracle Resource Manager, which is an OCI managed service that supports Terraform. Our Terraform stack creates the following OCI resources:
Network resources
Network resources include a Virtual Cloud Network (VCN), public and private subnets, and the corresponding route table, internet gateway and security list. Security list ingress rules allow traffic on port 22 for SSH access to the compute instance.
- oci_core_vcn: Virtual Cloud Network (VCN) in specified compartment. See VCNs and Subnets
- oci_core_subnet: Public subnet resource in the specified VCN.
- oci_core_subnet: Private subnet resource in the specified VCN.
- oci_core_internet_gateway: Internet gateway resource in the specified VCN.
- oci_core_route_table: Route table resource in the specified VCN.
- oci_core_security_list: Security list resource in the specified VCN see Security Lists
Compute resources
Compute resources include the compute instance and a block volume, which is where Docker and Docker Compose will be installed.
oci_core_volume: Block volume resource in the specified compartment.
oci_core_instance: Compute instance resource in the specified compartment.
oci_core_volume_attachment: Attaches the specified storage volume to the specified instance.
Using variables for portability
Our example uses variables and a schema document in order to let you customize aspects of the behavior without altering code. This allows you to share modules across different configurations, making your module composable and reusable.
There are a number of variables that are employed by the stack.
- tenancy_ocid: Tenancy OCID automatically populated by OCI. See Terraform Configurations for Resource Manager
- region: Region name automatically populated by OCI. See Terraform Configurations for Resource Manager
- compartment_ocid: Compartment OCID automatically populated by OCI. See Terraform Configurations for Resource Manager
- ssh_public_key: SSH public key used to login to compute instance.
- compute_display_name: Compute display name
- vcn_cidr_block: Virtual cloud network CIDR block. Defaults to 10.0.0.0/16
- vcn_public_subnet_cidr_block: Public subnet CIDR block. Defaults to 10.0.0.0/24
- vcn_private_subnet_cidr_block: Private subnet CIDR block. Defaults to 10.0.1.0/24
- ad: Availability domain to deploy resources
- image_operating_system: Compute image operating system Defaults to Oracle Linux
- instance_shape: Compute image shape. Defaults to VM.Standard.E2.1.Micro
- mount_dir: Block volume mount directory. Defaults to /scratch
- volume_size_in_gbs: Block volume size (GB). Defaults to 50
- docker_compose_version: Docker Compose version
Initialize your instance with cloud-init
Cloud-init is software for cloud instances that will set up the instance automatically using the provided metadata. This metadata describes how your server should look like, for example which packages should be installed and which commands are executed on init.
Cloud images are operating system templates and every instance starts out as an identical clone of every other instance. It is the user data that gives every cloud instance its personality and cloud-init is the tool that applies user data to your instances automatically.
Script steps
Cloud-init supports a number of user data formats including YAML config and shell scripts. Our example uses a shell scripts that executes the follwing steps:
- Mount block volume storage. Docker container images may excceed the boot volume size, so mount the attached block volume at the location specified by the mount_dir variable.
- Install Docker. Install Docker using the yum server and add the default user to the docker group. Uses docker_repo_url and username variables.
- Install Docker Compose. Install Docker Compose version specified by docker_compose_version variable.
- Update Docker Location. Update the Docker root folder specified in daemon.json file to the mount_dir location.
- Start Docker. Start the servce. To check if you have Docker installed, run command docker ps or docker info.
Template file with variables
In Terraform, you create a template_file data source, which contains the contents of your script plus any variable values, and pass that to your oci_core_instance using the metadata argument. See below for an example of the template_file with variables included:
data "template_file" "user_data" {
template = file("oci-cloud-init.sh")
vars = {
username = var.username
docker_repo_url = var.docker_repo_url
docker_compose_version = var.docker_compose_version
mount_dir = var.mount_dir
}
}
Pass metadata to cloud-init
You can use the following metadata key names to provide information to Cloud-Init
- ssh_authorized_keys: Provide one or more public SSH keys to be included in the
~/.ssh/authorized_keysfile for the default user on the instance. - user_data: Provide your own base64-encoded data to be used by Cloud-Init to run custom scripts or provide custom Cloud-Init configuration.
See below for an example of how you pass metadata to the instance:
resource "oci_core_instance" "test_server" {
...
metadata = {
ssh_authorized_keys = var.ssh_public_key
user_data = base64encode(data.template_file.user_data.rendered)
}
}
Getting started
When you sign up for an Oracle Cloud Infrastructure account, you’re assigned a secure and isolated partition within the cloud infrastructure called a tenancy. The tenancy is a logical concept and you can think of it as a root container where you create, organize, and administer your cloud resources.
The second logical concept used for organizing and controlling access to cloud resources is compartments. A compartment is a collection of related cloud resources. Every time you create a cloud resource, you must specify the compartment that you want the resource to belong to.
Ensure you have access to a compartment in your tenancy as well as Resource Manager.
Create stack
Follow the instructions to create a stack from a zip file or use the oci resource-manager stack create command and required parameters to create a stack from a local zip file.
Example request:
oci resource-manager stack create --compartment-id ${compartment.ocid} --config-source ${zipfile} --variables file://variables.json --display-name "Test Stack" --description "Example stack" --working-directory ""
Note, the variables parameter allows you to pass through Terraform variables associated with this resource. Example: {“vcn_cidr_block”: “10.0.0.0/16”} This is a complex type whose value must be valid JSON. The value can be provided as a string on the command line or passed in as a file using the file://path/to/file syntax.
Apply stack
When you run an apply job for a stack, Terraform provisions the resources and executes the actions defined in your Terraform configuration, applying the execution plan to the associated stack to create your Oracle Cloud Infrastructure resources. We recommend running a plan job (generating an execution plan) before running an apply job.
Use the oci resource-manager job create-apply-job command and required parameters to run an apply job.
Don’t forget to check out the sample code here: orm-docker-compute-stack
Happy hunting!
~sn
