Julian Fischer
Published at 03.02.2020
Cloud Foundry is a platform technology enabling multi-cloud deployments. It makes deploying applications to multiple-cloud platforms simple.
Table of Contents
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
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.
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.
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.
The application has now been successfully deployed and is ready to be used.
Have a look here: https://gostock.de.a9sapp.eu/stocks
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
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
Products & Services
© anynines GmbH 2024