Using Terraform with Oracle PaaS Service Manager (PSM)

Stephen Cross
Oracle Developers
Published in
4 min readJun 13, 2018

While the Terraform provider for Oracle Cloud Platform supports a growing number of resources for deploying and provisioning Oracle PaaS services on Oracle Cloud, not all PaaS services are currently supported by the provider. So what can you do if you need to provision an additional service as part of your Terraform configuration that is not yet supported by the Oracle Cloud Platform provider? One approach is to use Terraform in combination with the Oracle PaaS Service Manager (PSM) CLI tool.

Limitations of using Terraform with PSM

Before getting into to the details of how to use PSM with Terraform, its worth highlighting up front some of the limitations of this approach.

  • PSM CLI must be installed on local machine running terraform
  • Using PSM with Terraform for service creation only handles create/destroy use cases. Modifying the instance configuration in Terraform will result in the instance being destroyed and recreated.
  • Terraform will not detect or correct any delta between the desired and current provisioned state.
  • Do not run multiple provisioning tasks targeting different environments at the same time as they can overwrite each others settings.

Invoking PSM from Terraform

Terraform will be invoking PSM using a local-exec provisioner, so the PSM cli tool must be installed on the local host that is running Terraform. Refer to the PaaS Service Manager documentation for installation instructions.

$ psm --version
Oracle PaaS CLI client
Version 1.1.23

If you went through the steps for Configuring the Command Line Interface you will see that the PSM tool is configured with a single set of credentials for one target environment, these settings are stored for the local user account under ~/.psm/.

To ensure PSM is targeting the correct environment that Terraform is provisioning we need to call psm setup prior to each set of PSM provisioning commands, passing in the target environment details. We can use the local_file resource to create the required setup configuration file

resource "local_file" "psm_setup" {
filename = "psm-setup-payload.json"
content = <<JSON
{
"username":"${var.psm_user}",
"password":"${var.psm_password}",
"identityDomain":"${var.psm_domain}"
}
JSON
}
data "local_file" "psm_setup" {
depends_on = [ "local_file.psm_setup" ]
filename = "${local_file.psm_setup.filename}"
}

and call psm setup using thelocal-exec provisioner, passing the local config file name to the --config-payload option.

resource ... {  provisioner "local-exec" {
command = "psm setup --config-payload ${data.local_file.psm_setup.filename}"
}
...
}

Any subsequent PSM calls will now target the configured environment.

Provisioning PaaS Services with PSM

The PSM cli provisions new services by passing a service configuration JSON payload to the create-service action for the desired service. Example payloads for each service are available in the PSM documentation and by running the help command from the CLI, e.g.

$ psm IntegrationCloud create-service help

To invoke PSM from Terraform with the required service creation payload we follow the same pattern used above to perform the PSM setup, namely:

  1. use a local_file resource to create the JSON payload, setting the appropriate attributes values
  2. call the desired PSM cli command, referencing the configuration file
resource "local_file" "integration_service" {
filename = "integration-cloud-${var.service_name}-create-service.json"
content = <<JSON
{
"vmPublicKeyText": "${file("~/.ssh/id_rsa.pub")}",
"featureSet": "ics",
"serviceName": "icsdemo1",
"description": "created by Terraform and PSM",
"cloudStorageContainer": "${var.backup_storage_container}",
"cloudStorageUser": "${var.backup_storage_user}",
"cloudStoragePassword": "${var.backup_storage_password}",
"confirmation": true,
"meteringFrequency": "HOURLY",
"components": {
"WLS": {
"dbServiceName": "${var.database_service_name}",
"dbaName": "${var.database_user}",
"dbaPassword": "${var.database_password}",
"managedServerCount": 2
}
}
}
JSON
}
data "local_file" "integration_service" {
depends_on = [ "local_file.integration_service" ]
filename = "${local_file.integration_service.filename}"
}
resource null_resource "integration_service" { triggers {
payload = "${md5(local_file.integration_service.content)}"
}
provisioner "local-exec" {
command = "psm setup --config-payload ${data.local_file.psm_setup.filename}"
}

provisioner "local-exec" {
command = "psm IntegrationCloud create-service --wait-until-complete true --config-payload ${data.local_file.integration_service.filename}" }
}

Note the psm setup command is executed prior to create-service, this is to ensure the correct environment settings are applied for this specific deployment.

An MD5 hash of the configuration content is used in the triggers block so any changes to the configuration will trigger the resource to be recreated.

Adding destroy time provisioners

To ensure the PaaS service is destroyed on a terraform destroy, or when the service definition is changes and the instance need to be recreated, we need to add a destroy time provisioner. Again, in addition to the main PSM command to be executed we call psm setup to ensure the correct environment is targeted, as the PSM configuration may have been changed since the original apply.

resource null_resource "integration_service" {

...
provisioner "local-exec" {
when = "destroy"
command = "psm setup --config-payload ${data.local_file.psm_setup.filename}"
}
provisioner "local-exec" {
when = "destroy"
command = "psm IntegrationCloud delete-service --wait-until-complete true --service-name ${var.service_name} --dba-name ${var.database_user} --dba-password ${var.database_password} --skip-backup-on-terminate ${var.skip_backup_on_terminate} --force true"
}
}

Getting attributes from a PaaS Service instance

The attributes of a provisioned instance, whether the instance was provisioned using the above approach, or was already provisioned, can be read back into Terraform using the PSM cli as an external data source

The external data source type executes a local command that returns a JSON data structure, and puts the results into the Terraform state. Unfortunately Terraform does not fully support the JSON style returned from the PSM commands, however this can be fixed with a simple wrapper script to extract the required attributes and return a simplified JSON response.

The following psm_get_jcs.sh example gets the Java Cloud Service instance response and extracts a subset of attributes using jq

#!/usr/bin/env bash
# usage psm_get_jcs.sh <service-name>
RESP=$(psm jcs service -s $1 -of json)echo $RESP | jq "{ service_name: .serviceName, fmw_root: .FMW_ROOT, wls_root: .WLS_ROOT }"

The above is just a limited example, additional attributes added depending on the your specific requirements. To test the output of the wrapper script:

$ ./psm_get_jcs.sh jcsdemo1
{
"service_name":
"jcsdemo1",
"fmw_root":
"https://129.144.208.50:7002/em",
"wls_root":
"https://129.144.208.50:7002/console"
}

When invoking the wrapper script from the external data source the JSON attributes returned from the script are available under the data sources result attribute. e.g. to get the IP address of a Java Cloud Service instance WLS service instance:

data "external" "jcs" {
program = ["./psm_get_jcs.sh", "${var.service-name}"]
}
output "wls_ip" {
value = "${replace(element(split(":",data.external.jcs.result.wls_root),1),"////","")}"
}

--

--