anynines website

Categories

Series

Julian Fischer

Published at 03.02.2020

How-To’s & Tutorials

Multi-Cloud Deployments With Cloud Foundry

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

Table of Contents

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:

Code Example

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

Subsequently, stored stock prices can be queried by issuing:

Code Example

1curl http://localhost:8080/stocks

Screenshot illustrating the JSON response retrieving stock entries.

The code:

Code Example

1package main
2
3import (
4  "fmt"
5  "time"
6  "github.com/go-xorm/xorm"
7  "github.com/kataras/iris"
8  "github.com/kataras/iris/middleware/logger"
9  "github.com/kataras/iris/middleware/recover"
10  "github.com/lib/pq"
11  "github.com/cloudfoundry-community/go-cfenv"
12)
13
14// Stock represents a single stock title.
15
16type Stock struct {
17  ID   int64  // auto-increment by-default by xorm
18  Name string `form:"name" json:"name" validate:"required" xorm:"varchar(200)"`
19
20  // Value of the stock in EUR cent (no decimals)
21  Value     uint32    `form:"value" json:"value" validate:"required"`
22  CreatedAt time.Time `xorm:"created"`
23  UpdatedAt time.Time `xorm:"updated"`
24}
25
26var app *iris.Application
27var orm *xorm.Engine
28
29func main() {
30  app = iris.New()
31  app.Logger().SetLevel("debug")
32
33  // recover panics
34  app.Use(recover.New())
35  app.Use(logger.New())
36
37  // Establish DB connection
38  orm = connectDatabase()
39  createDbSchema()
40  seedData()
41
42  // Method: GET
43  // Resource http://localhost:8080
44  app.Handle("GET", "/stocks", listStocks)
45  app.Post("/stock", postStock)
46  app.Run(iris.Addr(":8080"), iris.WithoutServerError(iris.ErrServerClosed))
47}
48
49func databaseConnectionString() string {
50  appEnv, _ := cfenv.Current()
51  
52  fmt.Println("Services:", appEnv.Services)
53  
54  var a9spg cfenv.Service
55  
56  if appEnv.Services["a9s-postgresql10"] != nil {
57    a9spg = appEnv.Services["a9s-postgresql10"][0]
58  } else if appEnv.Services["elephantsql"] != nil {
59    a9spg = appEnv.Services["elephantsql"][0]
60  } else {
61    app.Logger().Fatalf("Neither a9s PostgreSQL nor ElephantSQL has been found. I cannot do without. Please bind a service instance to the app.")
62  }
63
64  dbConnectionString := fmt.Sprintf("%s?sslmode=disable", a9spg.Credentials["uri"])
65
66  // Note that this demo app logs the connection string including passwords to facilitate debugging during deployment.
67  app.Logger().Debug("DB Connection String: ", dbConnectionString)
68
69  return dbConnectionString
70}
71
72func connectDatabase() *xorm.Engine {
73  connString := databaseConnectionString()
74  orm, err := xorm.NewEngine("postgres", connString)
75
76  if err != nil {
77    app.Logger().Fatalf("ORM failed to initialized: %v", err)
78  }
79
80  // Close ORM later
81  iris.RegisterOnInterrupt(func() {
82    orm.Close()
83  })
84
85  return orm
86}
87
88func createDbSchema() {
89  // Create schema
90  err := orm.Sync2(new(Stock))
91
92  if err != nil {
93    app.Logger().Fatalf("Cannot create db schema. Maybe I was unable to connect to the DB: ", err)
94  }
95}
96
97func seedData() {
98  // Seed
99  count, err := orm.Count(new(Stock))
100
101  if err != nil {
102    app.Logger().Fatalf("Cannot retrieve data from db during seed check: ", err)
103  }
104
105  if count == 0 {
106    orm.Insert(&Stock{Name: "Apple", Value: 17780}, &Stock{Name: "Alphabet Inc Class A", Value: 102140})
107  }
108}
109
110
111
112func listStocks(ctx iris.Context) {
113  stocks := make(map[int64]Stock)
114  err := orm.Find(&stocks)
115
116  app.Logger().Debug("Stocks: ", stocks)
117
118  if err != nil {
119    app.Logger().Fatalf("orm failed to load stocks: %v", err)
120  }
121
122  ctx.JSON(iris.Map{"stocks": stocks})
123}
124
125func postStock(ctx iris.Context) {
126  stock := Stock{}
127  err := ctx.ReadForm(&stock)
128
129  if err != nil {
130    app.Logger().Errorf("Couldn't read form input in postStock: %v", err)
131  }
132
133  app.Logger().Debug("Submitted Stock:", stock)
134  orm.Insert(&stock)
135
136  // read stock from params
137  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.

Code Example

1cf:gostock demo$ cf login
2Noeud final de l'API : https://api.de.a9s.eu
3
4Email> demo@anynines.com
5
6Password>
7Authentification...
8OK
9
10Sélectionnez une organisation (ou appuyez sur Entrée pour ignorer) :
111. demo_anynines_com
12
13Org> 4
14Organisation ciblée demo_anynines_com
15
16Sélectionnez un espace (ou appuyez sur Entrée pour ignorer) :
171. production
182. staging
193. test
20
21Space> 1
22Espace ciblé production
23
24Noeud final d'API : https://api.de.a9s.eu (Version de l'API : 2.131.0)
25Utilisateur : demo@anynines.com
26Organisation : demo_anynines_com
27Espace : 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:

Code Example

1cf 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?

Code Example

1cf push

returns:

Code Example

1Start unsuccessful

A closer look with

Code Example

1cf logs gostock --recent

reveals within a quite lengthy list of log entries:

Code Example

1Cannot 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:

Code Example

1{
2  "VCAP_SERVICES": {
3  "a9s-postgresql10": [{
4    "binding_name": null,
5    "credentials": {
6      "host": "pgd673318-psql-master-alias.node.dc1.a9ssvc",
7      "hosts": [
8        "pgd673318-pg-0.node.dc1.a9ssvc"
9      ],
10      "name": "pgd673318",
11      "password": "a-random-password",
12      "port": 5432,
13      "uri": "postgres://a9sb9438b9545053269280dcfa501d9a713b6624d39:a-random-password@pgd673318-psql-master-alias.node.dc1.a9ssvc:5432/pgd673318",
14"username": "a9sb9438b9545053269280dcfa501d9a713b6624d39"
15    },
16    "instance_name": "gostockdb",
17    "label": "a9s-postgresql10",
18    "name": "gostockdb",
19    "plan": "postgresql-single-nano",
20    "provider": null,
21    "syslog_drain_url": null,
22    "tags": [
23      "sql",
24      "database",
25      "object-relational",
26      "consistent"
27    ],
28    "volume_mounts": []
29  }]
30  }
31}

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:

Code Example

1cf 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.

Code Example

1cf 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.

Code Example

1cf 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:

Code Example

1cf 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:

Code Example

1cf create-service elephantsql turtle gostockdb

Pushing the application:

Code Example

1cf push gostock

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

Code Example

1cf bind-service gostock gostockdb

A brief restage

Code Example

1cf 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.

© anynines GmbH 2024

Imprint

Privacy Policy

About

© anynines GmbH 2024