Multi-Cloud Deployments With Cloud Foundry

Article Header Image Multi-Cloud Deployments with Cloud Foundry by Julian Fischer

Cloud Foundry is a platform technology enabling multi-cloud deployments. It makes deploying applications to multiple-cloud platforms simple.

An Exemplary Stock Price App

Imagine a simple microservice to store and retrieve recent stock prices that shall be developed and deployed to two different platforms in two separate regions.

The application is written in Golang and kept to the bare minimum. It accepts incoming stock price updates in the format:

curl -d "name=BASF&value=6523" -X POST http://localhost:8080/stock

Subsequently, stored stock prices can be queried by issuing:

curl http://localhost:8080/stocks
Screenshot illustrating the JSON response retrieving stock entries.

The code:

package main

import (
  "fmt"
  "time"
  "github.com/go-xorm/xorm"
  "github.com/kataras/iris"
  "github.com/kataras/iris/middleware/logger"
  "github.com/kataras/iris/middleware/recover"
  "github.com/lib/pq"
  "github.com/cloudfoundry-community/go-cfenv"
)

// Stock represents a single stock title.

type Stock struct {
  ID   int64  // auto-increment by-default by xorm
  Name string `form:"name" json:"name" validate:"required" xorm:"varchar(200)"`

  // Value of the stock in EUR cent (no decimals)
  Value     uint32    `form:"value" json:"value" validate:"required"`
  CreatedAt time.Time `xorm:"created"`
  UpdatedAt time.Time `xorm:"updated"`
}

var app *iris.Application
var orm *xorm.Engine

func main() {
  app = iris.New()
  app.Logger().SetLevel("debug")

  // recover panics
  app.Use(recover.New())
  app.Use(logger.New())

  // Establish DB connection
  orm = connectDatabase()
  createDbSchema()
  seedData()

  // Method: GET
  // Resource http://localhost:8080
  app.Handle("GET", "/stocks", listStocks)
  app.Post("/stock", postStock)
  app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
}

func databaseConnectionString() string {
  appEnv, _ := cfenv.Current()
  
  fmt.Println("Services:", appEnv.Services)
  
  var a9spg cfenv.Service
  
  if appEnv.Services["a9s-postgresql10"] != nil {
    a9spg = appEnv.Services["a9s-postgresql10"][0]
  } else if appEnv.Services["elephantsql"] != nil {
    a9spg = appEnv.Services["elephantsql"][0]
  } else {
    app.Logger().Fatalf("Neither a9s PostgreSQL nor ElephantSQL has been found. I cannot do without. Please bind a service instance to the app.")
  }

  dbConnectionString := fmt.Sprintf("%s?sslmode=disable", a9spg.Credentials["uri"])

  // Note that this demo app logs the connection string including passwords to facilitate debugging during deployment.
  app.Logger().Debug("DB Connection String: ", dbConnectionString)

  return dbConnectionString
}

func connectDatabase() *xorm.Engine {
  connString := databaseConnectionString()
  orm, err := xorm.NewEngine("postgres", connString)

  if err != nil {
    app.Logger().Fatalf("ORM failed to initialized: %v", err)
  }

  // Close ORM later
  iris.RegisterOnInterrupt(func() {
    orm.Close()
  })

  return orm
}

func createDbSchema() {
  // Create schema
  err := orm.Sync2(new(Stock))

  if err != nil {
    app.Logger().Fatalf("Cannot create db schema. Maybe I was unable to connect to the DB: ", err)
  }
}

func seedData() {
  // Seed
  count, err := orm.Count(new(Stock))

  if err != nil {
    app.Logger().Fatalf("Cannot retrieve data from db during seed check: ", err)
  }

  if count == 0 {
    orm.Insert(&Stock{Name: "Apple", Value: 17780}, &Stock{Name: "Alphabet Inc Class A", Value: 102140})
  }
}



func listStocks(ctx iris.Context) {
  stocks := make(map[int64]Stock)
  err := orm.Find(&stocks)

  app.Logger().Debug("Stocks: ", stocks)

  if err != nil {
    app.Logger().Fatalf("orm failed to load stocks: %v", err)
  }

  ctx.JSON(iris.Map{"stocks": stocks})
}

func postStock(ctx iris.Context) {
  stock := Stock{}
  err := ctx.ReadForm(&stock)

  if err != nil {
    app.Logger().Errorf("Couldn't read form input in postStock: %v", err)
  }

  app.Logger().Debug("Submitted Stock:", stock)
  orm.Insert(&stock)

  // read stock from params
  ctx.JSON(iris.Map{"success": true})

Link to the github repository: https://github.com/fischerjulian/gostock

Platform Registration

To deploy to Cloud Foundry, developer access is required. Most platform providers offer a trial or free tier to test the platform and/or run a simple app such as the Stock app.

There are multiple Cloud Foundry providers and the registration process differs among them. One such offering is paas.anynines.com. The registration process is self-explanatory.

paas.anynines.com registration form

After finishing the sign-up process the target Cloud Foundry environment can be set:

cf api https://api.de.a9s.eu

This tells the local cf command to which Cloud Foundry to send API calls.

A login authenticates rightful application developers.

cf:gostock demo$ cf login
Noeud final de l'API : https://api.de.a9s.eu

Email> demo@anynines.com

Password>
Authentification...
OK

Sélectionnez une organisation (ou appuyez sur Entrée pour ignorer) :
1. demo_anynines_com

Org> 4
Organisation ciblée demo_anynines_com

Sélectionnez un espace (ou appuyez sur Entrée pour ignorer) :
1. production
2. staging
3. test

Space> 1
Espace ciblé production

Noeud final d'API : https://api.de.a9s.eu (Version de l'API : 2.131.0)
Utilisateur : demo@anynines.com
Organisation : demo_anynines_com
Espace : production

The login process leads to the selection of the Cloud Foundry organization and space. Organizations and spaces represent tenants on a Cloud Foundry platform and enable each tenant to represent further organizational or functional units.

A common pattern is to divide an organization using spaces into different development environments such as production, staging, and testing. However, this is neither mandatory nor a strong convention.

Once an organization and space are selected, the application deployment can begin.

Application Deployment via cf push

Deploying in application with Cloud Foundry is ultimately simple:

cf push

What happens is that the cf command uploads the application to Cloud Foundry, triggering the staging process. Staging includes probing a list of so-called buildpacks configured by the CF provider until a buildpack has matched the application.

A buildpack is a library dedicated to providing the appropriate runtime environment for a given application.

Once the application has been staged, a droplet is created containing an executable version of the stock app. For Cloud Foundry all droplets are equal. They represent executable applications no matter what‘s inside.

After staging the application, rather than executing the application directly, the staging environment is destroyed and an auction is started. Cloud Foundry announces the desire to execute the stock app with 2 application instances.

As a result of the auction, the application is being started at two locations within the Cloud Foundry application runtime currently called Diego.

The app is now running or is it?

cf push

returns:

Start unsuccessful

A closer look with

cf logs gostock --recent

reveals within a quite lengthy list of log entries:

Cannot create db schema: %!(EXTRA *net.OpError=dial tcp 127.0.0.1:5432: connect: connection refused)

So the app has not been started because the staging has failed due to a missing dependency: the database.

Persistency Using Data Services

As an opinionated platform, Cloud Foundry requires the web service itself to be stateless and delegate persistency to a separate data service instance. The stock app uses PostgreSQL as its data store.

The application assumes there is an environment variable found in its execution environment with the following structure:

{
  "VCAP_SERVICES": {
  "a9s-postgresql10": [{
    "binding_name": null,
    "credentials": {
      "host": "pgd673318-psql-master-alias.node.dc1.a9ssvc",
      "hosts": [
        "pgd673318-pg-0.node.dc1.a9ssvc"
      ],
      "name": "pgd673318",
      "password": "a-random-password",
      "port": 5432,
      "uri": "postgres://a9sb9438b9545053269280dcfa501d9a713b6624d39:a-random-password@pgd673318-psql-master-alias.node.dc1.a9ssvc:5432/pgd673318",
"username": "a9sb9438b9545053269280dcfa501d9a713b6624d39"
    },
    "instance_name": "gostockdb",
    "label": "a9s-postgresql10",
    "name": "gostockdb",
    "plan": "postgresql-single-nano",
    "provider": null,
    "syslog_drain_url": null,
    "tags": [
      "sql",
      "database",
      "object-relational",
      "consistent"
    ],
    "volume_mounts": []
  }]
  }
}

The application reads access credentials to the RBDMS from the VCAP_SERVICES environment variable and parsing it as a JSON data structure. While the general structure of this JSON structure is defined in the Open Service Broker API, it may differ among providers in detail.

But where does the PostgreSQL service come from? Technically, the Cloud Foundry application runtime does not provide data services. However, most Cloud Foundry providers offer a standard set of data services. These are integrated using so-called Service Brokers representing standard APIs to describe, provision and bind data service instances to be used by application developers.

So to store stock prices persistently, it is necessary to create a PostgreSQL service instance, first:

cf create-service a9s-postgresql10 postgresql-single-nano gostockdb

A few minutes later, a dedicated PostgreSQL database VM has been provisioned. The newly created service instance has the name gostockdb as specified in the create command.

However, the application cannot use it yet, as it lacks the necessary access credentials mentioned before. To grant the application access, a database user has to be created. This process is called creating a service binding in Cloud Foundry.

cf bind-service gostock gostockdb

The service binding represents a unique set of credentials specific to the connection of this particular application to this particular service instance.

Now, where the dependencies of the stock application are met, we can resume the application deployment.

cf restage gostock

Restaging triggers the execution of the buildpack again. The Java buildpack, for example, recognizes service bindings and injects database functionality to the container.

Accessing the App

The application has now been successfully deployed and is ready to be used.

Have a look here: https://gostock.de.a9sapp.eu/stocks

Deploying to the 2nd Platform

So far the application has been deployed to a single platform. Time to have a look at how the deployment to a second platform – run.pivotal.io also known as PWS – will look.

After creating an account obtained credentials can be used to set the API endpoint and login:

cf login -a api.run.pivotal.io

Again choose an organization and space.

As with paas.anynines.com, a PostgreSQL database is required to run the application. However, each Cloud Foundry provider may provide different data services with varying implementations among common ones.

On PWS, a PostgreSQL database can be created using ElephantSQL which will do the trick for our demo app. However, for production workloads a closer look at the data service implementation is necessary.

Two PostgreSQL databases can be vastly different in both performance and security. While a database on a shared PostgreSQL server may be sufficient for toy apps, a highly frequented application may require a solid dedicated PostgreSQL environment with three dedicated nodes and replication to provide fast failover options.

Creating the PostgreSQL database is similarly easy:

cf create-service elephantsql turtle gostockdb

Pushing the application:

cf push gostock

And then bind the database service instance to it works the same.

cf bind-service gostock gostockdb

A brief restage

cf restage gostock

and the application can be used on a second platform.

Have a look here: https://gostock.cfapps.io/stocks

Summary

This example shows how multi-cloud deployments can be facilitated using Cloud Foundry. The cf push user experience makes operating applications easy. Applications that have been deployed to one Cloud Foundry will run on other Cloud Foundry platforms, too.

This is true as long as the platform provider has solved the data challenge. Offering a standard set of data services in a fully automated on-demand self-service fashion, well integrated into the platform using service brokers.

This enables application developers to use the cf marketplace to create data service instances that complete Cloud Foundry to become a platform for truly portable multi-cloud applications.

Leave a Reply

Your email address will not be published. Required fields are marked *