Advanced Blueprint Example

In this tutorial we will create a blueprint that describes the topology of the Nodecellar application. We will write, step by step, a simplified version of the Single Host Nodecellar blueprint.

It includes the following components:

Infrastructure:

Middleware:

Application:

The topology looks like this:

nodecllar app

Cloudify YAML DSL

Cloudify’s Domain Specific Language (DSL) is written in YAML. If you are not familiar with yaml you may want to read the YAML Documentation first.

Step by Step Walk-through

Step 1: Creating the Blueprint

First lets create a folder with the name nodecellar and create a blueprint.yaml file within it. This file is the blueprint file. Lets also give our blueprint a version:

tosca_definitions_version: cloudify_dsl_1_2

To learn more about blueprint versioning, refer to Versions.

Step 2: Adding imports

The imports section is usually the first statement in our blueprint. It defines references to external YAML files that may hold definitions of various constructs:

For a complete reference of these constructs refer to the DSL Specification section.

In our case, we need to import Cloudify’s built-in types definitions since our custom node_types will derive from some of these types.

This will become clearer as we go along, don’t worry :)

To learn more about imports, please refer to Imports Specification.

So, basically, our blueprint file now looks like:

imports:
  - 
    

Step 3: Adding blueprint inputs

The inputs section is where you define which blueprint parameters will be configurable without the need to edit the blueprint file. To learn more about inputs, please refer to Inputs Specification.

In our case, we declare the connection details to our host as inputs, Like so:

inputs:
  host_ip:
    description: >
      The ip of the host the application will be deployed on
  agent_user:
    description: >
      User name used when SSH-ing into the started machine
  agent_private_key_path:
    description: >
      Path to a private key that resides on the management machine.
      SSH-ing into agent machines will be done with this key.

Our blueprint now looks like:

imports:
  - 
    


inputs:
  host_ip:
    description: >
      The ip of the host the application will be deployed on
  agent_user:
    description: >
      User name used when SSH-ing into the started machine
  agent_private_key_path:
    description: >
      Path to a private key that resided on the management machine.
      SSH-ing into agent machines will be done with this key.

Step 4: Adding node_types

Next up is adding the application specific types. We will have 3 new types:

node_types usage is two-fold:

  1. Define a properties schema that node_templates must adhere to.
  2. Define interface operation mappings.

To learn more about node_types, please refer to Node Types Specification.

Lets see an example and things will make more sense.

nodecellar.nodes.MongoDatabase:
  derived_from: cloudify.nodes.DBMS
  properties:
    port:
      description: MongoDB port
      type: integer
  interfaces:
    cloudify.interfaces.lifecycle:
      create: scripts/mongo/install-mongo.sh
      start: scripts/mongo/start-mongo.sh
      stop: scripts/mongo/stop-mongo.sh

So what do we have here?

#!/bin/bash -e
...
PORT=$(ctx node properties port)
...
ctx logger info "The port is ${PORT}"

For more examples of using the ctx utility refer to Context Proxy Utility.

In this case, the start-mongo.sh script uses the port property to configure the data base port. In the same manner, we define our additional types, to eventually get this blueprint:

imports:
  - 
    


inputs:
  host_ip:
    description: >
      The ip of the host the application will be deployed on
  agent_user:
    description: >
      User name used when SSH-ing into the started machine
  agent_private_key_path:
    description: >
      Path to a private key that resided on the management machine.
      SSH-ing into agent machines will be done with this key.

node_types:

  nodecellar.nodes.MongoDatabase:
    derived_from: cloudify.nodes.DBMS
    properties:
      port:
        description: MongoDB port
        type: integer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/mongo/install-mongo.sh
        start: scripts/mongo/start-mongo.sh
        stop: scripts/mongo/stop-mongo.sh

  nodecellar.nodes.NodeJSServer:
    derived_from: cloudify.nodes.ApplicationServer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/nodejs/install-nodejs.sh

  nodecellar.nodes.NodecellarApplicationModule:
    derived_from: cloudify.nodes.ApplicationModule
    properties:
      port:
        description: Web application port
        type: integer
      application_url:
        description: >
          URL to an archive containing the application source.
          The archive must contain one top level directory.
        default: https://github.com/cloudify-cosmo/nodecellar/archive/master.tar.gz
      startup_script:
        description: >
          This script will be used to start the nodejs application.
          The path is relative to the top level single directory inside
          the archive
        type: string
        default: server.js
    interfaces:
      cloudify.interfaces.lifecycle:
        configure: scripts/nodecellar/install-nodecellar-app.sh
        start: scripts/nodecellar/start-nodecellar-app.sh
        stop: scripts/nodecellar/stop-nodecellar-app.sh

)**

Also, two points are worth mentioning regarding the scripts:

  1. Should be written in an idempotent manner. As is it possible they will be executed several times per execution, on account of retires due to failures.
  2. Should be synchronous and wait for processes to start before exiting. For example, in the start-mongo.sh script we wait for mongo to run:

ctx logger info "${COMMAND}" nohup ${COMMAND} > /dev/null 2>&1 & PID=$!

MONGO_REST_PORT=expr ${PORT} + <span style="color:#ae81ff">1000</span> wait_for_server ${MONGO_REST_PORT} 'MongoDB'

<div class="panel panel-default">
<div class="panel-heading">Tip</div>
<div class="panel-body">

Think of node_types as a location to place shared interface implementations and properties. When your blueprint contains only one node template of a node type, it may not make much sense in defining these types, because all of this can be defined in the node template as well. However, node_types, as opposed to node_templates (see step 6), are importable. This means that you can place node_types in a different file, and have various blueprints import that file and use them. To learn more about this, have a look at the full blown [Nodecellar example](

)

Step 5: Adding relationships

The relationships section is where (as the name suggests) we define relationships to be later used by node_templates. In this application we can think of 2 relationships, both of which are related to the nodecellar.nodes.NodecellarApplicationModule type:

Cloudify’s built-in types definitions comes with a relationship of type cloudify.relationships.connected_to, which seems to fit into our case.

Cloudify’s built-in types definitions comes with a relationship of type cloudify.relationships.contained_in, which also seems appropriate.

Note that these relationships do not currently define any implementation of the relationship, since this is of course application dependent. What it does is define the basic operations one can implement. Similar to the lifecycle operation, relationship operations will also be invoked as part of the install workflow execution.

To learn more about relationships, please refer to Relationships Specification.

So, lets see how we use these relationships. First off, we will define our custom cloudify.relationships.connected_to relationship type:

relationships:
  node_connected_to_mongo:
    derived_from: cloudify.relationships.connected_to
    target_interfaces:
      cloudify.interfaces.relationship_lifecycle:
        postconfigure: scripts/mongo/set-mongo-url.sh

In this example, we create a new relationship type called node_connected_to_mongo. It is derived from the base relationship type we talked about, the cloudify.relationships.connected_to type. We map its target interface postconfigure operation to, you guessed it, a bash script. Each relationship has two edges, a source node and a target node, The definition we used here answers two questions:

  1. When will the set-mongo-url.sh script be executed?

Immediately after the target node’s configure lifecycle operation.

  1. Where will the set-mongo-url.sh script be executed?

On the VM hosting that target node.

This script uses the Context API to set runtime properties that determine the MongoDB URL on the source node of this relationship.

In the same manner, we define the second relationship, this should now be clear:

node_contained_in_nodejs:
  derived_from: cloudify.relationships.contained_in
  target_interfaces:
    cloudify.interfaces.relationship_lifecycle:
      preconfigure: scripts/nodejs/set-nodejs-root.sh

Our full blueprint now looks like:

imports:
  - 
    


inputs:
  host_ip:
    description: >
      The ip of the host the application will be deployed on
  agent_user:
    description: >
      User name used when SSH-ing into the started machine
  agent_private_key_path:
    description: >
      Path to a private key that resided on the management machine.
      SSH-ing into agent machines will be done with this key.

node_types:

  nodecellar.nodes.MongoDatabase:
    derived_from: cloudify.nodes.DBMS
    properties:
      port:
        description: MongoDB port
        type: integer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/mongo/install-mongo.sh
        start: scripts/mongo/start-mongo.sh
        stop: scripts/mongo/stop-mongo.sh

  nodecellar.nodes.NodeJSServer:
    derived_from: cloudify.nodes.ApplicationServer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/nodejs/install-nodejs.sh

  nodecellar.nodes.NodecellarApplicationModule:
    derived_from: cloudify.nodes.ApplicationModule
    properties:
      port:
        description: Web application port
        type: integer
      application_url:
        description: >
          URL to an archive containing the application source.
          The archive must contain one top level directory.
        default: https://github.com/cloudify-cosmo/nodecellar/archive/master.tar.gz
      startup_script:
        description: >
          This script will be used to start the nodejs application.
          The path is relative to the top level single directory inside
          the archive
        type: string
        default: server.js
    interfaces:
      cloudify.interfaces.lifecycle:
        configure: scripts/nodecellar/install-nodecellar-app.sh
        start: scripts/nodecellar/start-nodecellar-app.sh
        stop: scripts/nodecellar/stop-nodecellar-app.sh

relationships:

  node_connected_to_mongo:
    derived_from: cloudify.relationships.connected_to
    target_interfaces:
      cloudify.interfaces.relationship_lifecycle:
        postconfigure: scripts/mongo/set-mongo-url.sh

  node_contained_in_nodejs:
    derived_from: cloudify.relationships.contained_in
    target_interfaces:
      cloudify.interfaces.relationship_lifecycle:
        preconfigure: scripts/nodejs/set-nodejs-root.sh

Step 6: Adding node_templates

So far, we have mainly defined types, be it node_types or relationship types. Types themselves do not constitute a valid blueprint, they are meant to be used by node_templates, which are basically just occurrences of specific node_types.

To learn more about node_templates, please refer to Node Templates Specification.

Lets define our first node template. Until now we have only dealt with the Middleware and Application parts of the topology, but what about the Infrastructure? Remember that our infrastructure consists of just a single, pre-existing host. So we start by defining it.

host:
  type: cloudify.nodes.Compute

Just now, we defined a node template called host, and specified that its type is cloudify.nodes.Compute. This type is one that is provided by Cloudify’s built-in types definitions. We talked about node_types and the fact that they can define a properties schema. This is exactly what the cloudify.nodes.Compute does. Lets have a look:

cloudify.nodes.Compute:
  properties:
    install_agent:
      default: true
    cloudify_agent:
      default: {}
    ip:
      default: ''

so we have 3 properties defined here:

This is how we populate specific values for these properties:

host:
  type: cloudify.nodes.Compute
  properties:
    ip: localhost
    cloudify_agent:
      user: ubuntu
      key: /home/ubuntu/.ssh/agent_key.pem

However, at the beginning of this tutorial we talked about the inputs section, and said that we want these connection details to be configurable by outside users. We did so by adding several inputs, that map exactly to these details. But how do we use them?

This is where Intrinsic Functions come in to play. We use the get_input function to retrieve inputs defined in the blueprint.

host:
  type: cloudify.nodes.Compute
  properties:
    ip: { get_input: host_ip }
    cloudify_agent:
      user: { get_input: agent_user }
      key: { get_input: agent_private_key_path }

So, lets have a look at our blueprint so far:

imports:
  - 
    


inputs:
  host_ip:
    description: >
      The ip of the host the application will be deployed on
  agent_user:
    description: >
      User name used when SSH-ing into the started machine
  agent_private_key_path:
    description: >
      Path to a private key that resided on the management machine.
      SSH-ing into agent machines will be done with this key.

node_types:

  nodecellar.nodes.MongoDatabase:
    derived_from: cloudify.nodes.DBMS
    properties:
      port:
        description: MongoDB port
        type: integer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/mongo/install-mongo.sh
        start: scripts/mongo/start-mongo.sh
        stop: scripts/mongo/stop-mongo.sh

  nodecellar.nodes.NodeJSServer:
    derived_from: cloudify.nodes.ApplicationServer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/nodejs/install-nodejs.sh

  nodecellar.nodes.NodecellarApplicationModule:
    derived_from: cloudify.nodes.ApplicationModule
    properties:
      port:
        description: Web application port
        type: integer
      application_url:
        description: >
          URL to an archive containing the application source.
          The archive must contain one top level directory.
        default: https://github.com/cloudify-cosmo/nodecellar/archive/master.tar.gz
      startup_script:
        description: >
          This script will be used to start the nodejs application.
          The path is relative to the top level single directory inside
          the archive
        type: string
        default: server.js
    interfaces:
      cloudify.interfaces.lifecycle:
        configure: scripts/nodecellar/install-nodecellar-app.sh
        start: scripts/nodecellar/start-nodecellar-app.sh
        stop: scripts/nodecellar/stop-nodecellar-app.sh

relationships:

  node_connected_to_mongo:
    derived_from: cloudify.relationships.connected_to
    target_interfaces:
      cloudify.interfaces.relationship_lifecycle:
        postconfigure: scripts/mongo/set-mongo-url.sh

  node_contained_in_nodejs:
    derived_from: cloudify.relationships.contained_in
    target_interfaces:
      cloudify.interfaces.relationship_lifecycle:
        preconfigure: scripts/nodejs/set-nodejs-root.sh

node_templates:
  host:
    type: cloudify.nodes.Compute
    properties:
      ip: { get_input: host_ip }
      cloudify_agent:
        user: { get_input: agent_user }
        key: { get_input: agent_private_key_path }

Now we can add the rest of our application node_templates. We start with the Mongo database:

mongod:
  type: nodecellar.nodes.MongoDatabase
  properties:
    port: 27017
  relationships:
    - type: cloudify.relationships.contained_in
      target: host

Notice that we defined a relationship of type cloudify.relationships.contained_in and declare that this node template is contained inside our host. This relationship will ensure that the host node will be provisioned before the mongod node starts. Also, this is where we use our node_types that we defined earlier. We can see that the mongod node is of type nodecellar.nodes.MongoDatabase. The NodeJS node is very similar, and is actually even simpler:

nodejs:
  type: nodecellar.nodes.NodeJSServer
  relationships:
    - type: cloudify.relationships.contained_in
      target: host

The last node template we will define is our nodecellar application module:

nodecellar:
  type: nodecellar.nodes.NodecellarApplicationModule
  properties:
    port: 8080
  relationships:
    - type: node_connected_to_mongo
      target: mongod
    - type: node_contained_in_nodejs
      target: nodejs

The interesting part here is the relationships key. Notice we define two relationships, each one uses a relationship type we defined earlier, and stitches it to a specific node template.

So, we now have an almost complete blueprint:

imports:
  - 
    


inputs:
  host_ip:
    description: >
      The ip of the host the application will be deployed on
  agent_user:
    description: >
      User name used when SSH-ing into the started machine
  agent_private_key_path:
    description: >
      Path to a private key that resided on the management machine.
      SSH-ing into agent machines will be done with this key.

node_types:

  nodecellar.nodes.MongoDatabase:
    derived_from: cloudify.nodes.DBMS
    properties:
      port:
        description: MongoDB port
        type: integer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/mongo/install-mongo.sh
        start: scripts/mongo/start-mongo.sh
        stop: scripts/mongo/stop-mongo.sh

  nodecellar.nodes.NodeJSServer:
    derived_from: cloudify.nodes.ApplicationServer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/nodejs/install-nodejs.sh

  nodecellar.nodes.NodecellarApplicationModule:
    derived_from: cloudify.nodes.ApplicationModule
    properties:
      port:
        description: Web application port
        type: integer
      application_url:
        description: >
          URL to an archive containing the application source.
          The archive must contain one top level directory.
        default: https://github.com/cloudify-cosmo/nodecellar/archive/master.tar.gz
      startup_script:
        description: >
          This script will be used to start the nodejs application.
          The path is relative to the top level single directory inside
          the archive
        type: string
        default: server.js
    interfaces:
      cloudify.interfaces.lifecycle:
        configure: scripts/nodecellar/install-nodecellar-app.sh
        start: scripts/nodecellar/start-nodecellar-app.sh
        stop: scripts/nodecellar/stop-nodecellar-app.sh

relationships:

  node_connected_to_mongo:
    derived_from: cloudify.relationships.connected_to
    target_interfaces:
      cloudify.interfaces.relationship_lifecycle:
        postconfigure: scripts/mongo/set-mongo-url.sh

  node_contained_in_nodejs:
    derived_from: cloudify.relationships.contained_in
    target_interfaces:
      cloudify.interfaces.relationship_lifecycle:
        preconfigure: scripts/nodejs/set-nodejs-root.sh

node_templates:

  host:
    type: cloudify.nodes.Compute
    properties:
      ip: { get_input: host_ip }
      cloudify_agent:
        user: { get_input: agent_user }
        key: { get_input: agent_private_key_path }

  mongod:
    type: nodecellar.nodes.MongoDatabase
    properties:
      port: 27017
    relationships:
      - type: cloudify.relationships.contained_in
        target: host

  nodejs:
    type: nodecellar.nodes.NodeJSServer
    relationships:
      - type: cloudify.relationships.contained_in
        target: host

  nodecellar:
    type: nodecellar.nodes.NodecellarApplicationModule
    properties:
      port: 8080
    relationships:
      - type: node_connected_to_mongo
        target: mongod
      - type: node_contained_in_nodejs
        target: nodejs

Step 7: Adding outputs

The outputs part of the blueprint is optional, but it is useful because outputs allow the blueprint to expose application characteristics via the REST API or the CLI. In this blueprint we will use outputs to expose the application url endpoint, like so:

outputs:
  endpoint:
    description: Web application endpoint
    value:
      ip_address: { get_property: [host, ip] }
      port: { get_property: [nodecellar, port] }

We see that the outputs section defines one output called endpoint, this output in turn defines two keys:

Both of these values are retrieved by using another intrinsic function called get_property, which can extract properties from different nodes in the blueprint. After the install workflow has finished executing, we can run:

cfy deployments outputs -d <deployment_id>
 - "endpoint":
     Description: Web application endpoint
     Value: {u'ip_address': u'192.168.40.156', u'port': 8080}

Final Result

Lets take a look at our full blueprint:

imports:
  - 
    


inputs:
  host_ip:
    description: >
      The ip of the host the application will be deployed on
  agent_user:
    description: >
      User name used when SSH-ing into the started machine
  agent_private_key_path:
    description: >
      Path to a private key that resided on the management machine.
      SSH-ing into agent machines will be done with this key.

node_types:

  nodecellar.nodes.MongoDatabase:
    derived_from: cloudify.nodes.DBMS
    properties:
      port:
        description: MongoDB port
        type: integer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/mongo/install-mongo.sh
        start: scripts/mongo/start-mongo.sh
        stop: scripts/mongo/stop-mongo.sh

  nodecellar.nodes.NodeJSServer:
    derived_from: cloudify.nodes.ApplicationServer
    interfaces:
      cloudify.interfaces.lifecycle:
        create: scripts/nodejs/install-nodejs.sh

  nodecellar.nodes.NodecellarApplicationModule:
    derived_from: cloudify.nodes.ApplicationModule
    properties:
      port:
        description: Web application port
        type: integer
      application_url:
        description: >
          URL to an archive containing the application source.
          The archive must contain one top level directory.
        default: https://github.com/cloudify-cosmo/nodecellar/archive/master.tar.gz
      startup_script:
        description: >
          This script will be used to start the nodejs application.
          The path is relative to the top level single directory inside
          the archive
        type: string
        default: server.js
    interfaces:
      cloudify.interfaces.lifecycle:
        configure: scripts/nodecellar/install-nodecellar-app.sh
        start: scripts/nodecellar/start-nodecellar-app.sh
        stop: scripts/nodecellar/stop-nodecellar-app.sh

relationships:

  node_connected_to_mongo:
    derived_from: cloudify.relationships.connected_to
    target_interfaces:
      cloudify.interfaces.relationship_lifecycle:
        postconfigure: scripts/mongo/set-mongo-url.sh

  node_contained_in_nodejs:
    derived_from: cloudify.relationships.contained_in
    target_interfaces:
      cloudify.interfaces.relationship_lifecycle:
        preconfigure: scripts/nodejs/set-nodejs-root.sh

node_templates:

  host:
    type: cloudify.nodes.Compute
    properties:
      ip: { get_input: host_ip }
      cloudify_agent:
        user: { get_input: agent_user }
        key: { get_input: agent_private_key_path }

  mongod:
    type: nodecellar.nodes.MongoDatabase
    properties:
      port: 27017
    relationships:
      - type: cloudify.relationships.contained_in
        target: host

  nodejs:
    type: nodecellar.nodes.NodeJSServer
    relationships:
      - type: cloudify.relationships.contained_in
        target: host

  nodecellar:
    type: nodecellar.nodes.NodecellarApplicationModule
    properties:
      port: 8080
    relationships:
      - type: node_connected_to_mongo
        target: mongod
      - type: node_contained_in_nodejs
        target: nodejs

outputs:
  endpoint:
    description: Web application endpoint
    value:
      ip_address: { get_property: [host, ip] }
      port: { get_property: [nodecellar, port] }

That’s it! This a fully functioning blueprint that can be used with a Cloudify Management Environment to install the nodecellar application on an existing host.