In my lab, I wanted to setup Dynamic DNS (DDNS) so I did not need to pay for a static IP address from my ISP, because I’m cheap There are a few projects that can accomplish this, in my case I was looking to use ddns-route53, since my DNS zone is stored in AWS Route53.
My goal was to make it completely automated by code from start to finish, using Morpheus. I’m using a vSphere VM in this case to deploy the software, although a container and other options are available as well. Morpheus supports importing your Terraform code as-is into a Terraform App Blueprint but I decided to use Terraform Spec Templates in Morpheus, which will still import your code but allow the use of the native variables that Morpheus provides. Adding this functionality will augment and simplify our deployment.
Additionally, I wanted to have as much functionality as possible from Morpheus, so I also used the Morpheus Terraform Provider, which allows the use of Morpheus resources in your Terraform configuration. API calls are fully available for Morpheus but if you are experienced with using Terraform, you may want use the same technology.
The Code
Of course, all of the TF code could be placed into a single file but I prefer organization of the files. There are four files we’ll see below:
- provider.tf
- setupVM.tf
- setupAWS.tf
- variables.tf
provider.tf
file:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
morpheus = {
source = "morpheusdata.com/gomorpheus/morpheus"
version = "0.4.1"
}
}
backend "s3" {
# Replace this with your bucket name
bucket = "terraform-state-bucketname"
key = "global/s3/terraform.tfstate"
region = "us-east-1"
# Replace this with your DynamoDB table name
dynamodb_table = "terraform-state"
encrypt = true
access_key = "<%=cypher.read('secret/awsautomation').tokenize('|')[0]%>"
secret_key = "<%=cypher.read('secret/awsautomation').tokenize('|')[1]%>"
}
}
# Configure the AWS Provider
provider "aws" {
region = "us-east-1"
access_key = "<%=cypher.read('secret/awsautomation').tokenize('|')[0]%>"
secret_key = "<%=cypher.read('secret/awsautomation').tokenize('|')[1]%>"
}
provider "morpheus" {
url = "<%=cypher.read('secret/morpheusCreds').tokenize('|')[0]%>"
username = "<%=cypher.read('secret/morpheusCreds').tokenize('|')[1]%>"
password = "<%=cypher.read('secret/morpheusCreds').tokenize('|')[2]%>"
}
There are a few items to note in the providers.tf
above. As mentioned, we are using the Morpheus Terraform Provider, which you can see the configuration listed in the required_providers
section.
My existing deployments have used an S3 backend for the state file, so I continued to use it here as well. However, Morpheus can manage your state file for you, using a backend is not required and can make it more complex. Having Morpheus manage this is much easier by just omitting a backend configuration.
However, if you use backends, you may be familiar with not being able to use TF variables in the backend configuration. There are tools out there to try and overcome this, or using -backend-config at the command line or a backend file. One great feature for using Spec Templates is that we can use the Morpheus variables, which get translated prior to running the TF code. When Terraform is run, it looks like hard coded values to the backend configuration, so there are no complaints to be seen. You’ll see additional usage of variables being used in the providers to get other credentials from Cypher. This is an example of the contents of the secret/morpheusCreds
Cypher seen above:
https://morpheus.example.local|username|password
The two Cyphers seen in the file were requirements that had to be created prior to deployment, so Terraform could automate against my AWS environment and my Morpheus environment. The Morpheus Terraform Provider uses OAuth for authentication in the example above but other authentication options are available.
setupVM.tf
file:
data "morpheus_group" "morpheus_group" {
name = "All Clouds"
}
data "morpheus_cloud" "morpheus_cloud" {
name = "VMware - Lab"
}
data "morpheus_resource_pool" "vsphere_resource_pool" {
name = "Cluster01"
cloud_id = data.morpheus_cloud.morpheus_cloud.id
}
data "morpheus_instance_type" "ubuntu_instance_type" {
name = "Demo - Ubuntu"
}
data "morpheus_instance_layout" "ubuntu_layout" {
name = "Demo - Ubuntu VMware"
}
data "morpheus_network" "dhcp_network" {
name = "VLAN100-Servers-DHCP"
}
data "morpheus_plan" "cpu1gb1_plan" {
name = "1 CPU, 1GB Memory"
}
resource "morpheus_vsphere_instance" "tf_example_vsphere_instance" {
name = var.vm_name
description = "Terraform instance example"
group_id = data.morpheus_group.morpheus_group.id
cloud_id = data.morpheus_cloud.morpheus_cloud.id
resource_pool_id = data.morpheus_resource_pool.vsphere_resource_pool.id
instance_type_id = data.morpheus_instance_type.ubuntu_instance_type.id
instance_layout_id = data.morpheus_instance_layout.ubuntu_layout.id
plan_id = data.morpheus_plan.cpu1gb1_plan.id
workflow_id = 6
environment = "dev"
labels = ["ubuntu", "terraform"]
interfaces {
network_id = data.morpheus_network.dhcp_network.id
}
tags = {
name = var.vm_name
deploy_type = "terraform"
os_type = "ubuntu"
}
evar {
name = "application"
value = "demo"
export = true
masked = true
}
depends_on = [aws_iam_access_key.ddns_key]
}
In the setupVM.tf
above, you can see the Morpheus Terraform Provider in action. As I mentioned in the beginning, the reason we want to use the Morpheus provider, instead of the standard vSphere provider, is because we can use many of the Morpheus features we are accustom to during normal provisioning. For example, agent install, workflow execution, configurations, etc. If we used the standard vSphere provider, we’d need to do additional manual work or code to setup the agents and configuration.
As seen above, you can use data sources to provide the data to your resources or just had code the values in if preferred. You can configure many of the items we see daily in Morpheus when provisioning.
setupAWS.tf
file:
resource "aws_iam_user" "ddns_user" {
name = "ddns_user"
path = "/lab/"
}
resource "aws_iam_access_key" "ddns_key" {
user = aws_iam_user.ddns_user.name
provisioner "local-exec" {
# Configure Cypher
command = <<-EOT
$morpheusUrl = '<%=morpheus.applianceUrl%>'
$morpheusUsername = '<%=cypher.read('secret/morpheusCreds').tokenize('|')[1]%>'
$morpheusPassword = '<%=cypher.read('secret/morpheusCreds').tokenize('|')[2]%>'
$awsAccessId = '${aws_iam_access_key.ddns_key.id}'
$awsAccessSecret = '${aws_iam_access_key.ddns_key.secret}'
$access_token=(Invoke-RestMethod -Method POST -Uri "$($morpheusUrl)oauth/token?grant_type=password&scope=write&client_id=morph-api" -Body @{username="$morpheusUsername";password="$morpheusPassword"} -ContentType 'application/x-www-form-urlencoded' -SkipCertificateCheck).access_token
Invoke-RestMethod -Method POST -Uri "$($morpheusUrl)api/cypher/secret/ddns?type=string" -Headers @{Authorization="BEARER $access_token"} -ContentType 'application/json' -Body (@{value="$($awsAccessId)|$($awsAccessSecret)"}|ConvertTo-Json) -SkipCertificateCheck
EOT
interpreter = ["pwsh", "-Command"]
}
}
resource "aws_iam_user_policy" "ddns_policy" {
name = "ddns_policy"
user = aws_iam_user.ddns_user.name
policy = <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"route53:ChangeResourceRecordSets"
],
"Effect": "Allow",
"Resource": "arn:aws:route53:::hostedzone/${var.hosted_zone_id}"
}
]
}
EOF
}
In the setupAWS.tf
above, we see some of the same that we’ve seen, such as being able to use variables from Morpheus to be able to get secrets and other data, to keep our code as secure as possible but also portable, if needed.
In this example, we are using the standard AWS provider and creating an AWS user, access key, and policy so the service can update the DNS record as needed.
Note:
At the time of this writing the current version of the Morpheus Terraform Provider is v0.4.0 and does not support Cypher but many additional resources are coming soon. In the example above, we are using the local-exec Creation-Time Provisioner to create Cypher objects in Morpheus via the API. This Cypher is added so it can be used in the future with helping setup the service on the VM.
variables.tf
file:
variable "vm_name" {
type = string
}
variable "hosted_zone_id" {
type = string
}
Finally, we see the variables.tf
file, which contain the variables to help customize our configuration, which we’ve seen referenced in the previous files. Nothing magical here, just some organization
Spec Templates
Now that we have seen the code that will do the magic, we want to add these as Spec Templates from a repository and then to an Terraform App Blueprint.
- Navigate to:
Library > Templates > Spec Templates tab - Click the “Add” button to begin the adding of a new Spec Template
- Fill in the fields as appropriate, see below for an example:
- Repeat the process for each file that should be added for this deployment
App Blueprint
Once all the Spec Templates have been added to Morpheus, it is time to create an App Blueprint and add the Spec Templates to it
- Navigate to:
Library > Blueprints > App Blueprints tab - Click the “Add” button to begin the adding of a new Terraform Blueprint
- Fill in the fields as appropriate, see below for an example:
Deploy Blueprint
Finally, after the Blueprint has been created, it is time to deploy it!
- Navigate to:
Provisioning > Apps - Click the “Add” button and choose the blueprint created previously
- Choose the Morpheus group and Cloud this should be added to for organization
- Finally, enter the values for your variables and continue through the wizard
- The App will be seen in:
Provisioning > Apps
Once these items are provisioned, you can setup the service for ddns-route53 in the operating system, following their documentation. In my case, I have created an Ansible Playbook that is integrated into Morpheus, to help configure this server completely. The playbook uses the Cypher created in the setupAWS.tf
configuration, so two different technologies can be used to both create and bootstrap your deployments.
See those next steps in the Using Terraform and Ansible Together post!