-
Notifications
You must be signed in to change notification settings - Fork 1
/
cfpush.yaml
234 lines (162 loc) · 9.78 KB
/
cfpush.yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
- title: 'Welcome to cfpush ☁️ '
subtitle: an interactive Cloud Foundry tutorial in your terminal
body: |
We will be exploring **Cloud Foundry** and cloud-native computing by deploying a real chat application to Cloud Foundry.
- body: |
To begin, we should login.
When logging in we must identify the specific Cloud Foundry that we want to target. All the commands we run after that will run against that Cloud Foundry.
In our case, we'll log into Pivotal Web Services using its Single Sign-On. Once prompted, go to your browser, login and copy the temporary auth code.
command:
filename: cf
args: [login, -a, api.run.pivotal.io, --sso]
- body: |
We need somewhere to deploy our apps to.
Let's create a new space for this tutorial.
command:
filename: cf
args:
- create-space
- cfpush-tutorial
- body: |
We have created a new space. But we still have to set it as our current target.
command:
filename: cf
args: [target, -s, cfpush-tutorial]
- body: |
We are ready for deployment. Let's start with the frontend, the **chat-app**.
It is a Javascript React application that continuously polls a **message-service** for messages and allows to send new ones. It expects the **message-service** on its own host at _/api_.
And like any Javascript browser application, the **chat-app** is a collection of static files; a "_bundle_". We zipped our bundle into a zipfile located at _./builds/chat-app.zip_. Since we simply want to serve static files, we will use the "staticfile_buildpack" for running the app.
We push the app by pointing the cli at _./builds/chat-app.zip_, selecting the buildpack and letting Cloud Foundry pick a random available route for us.
command:
filename: cf
args: [push chat-app, -p, ./builds/chat-app.zip, -b, staticfile_buildpack, --random-route]
- body: |
Congratulations you have successfully deployed an application to Cloud Foundry!
The **chat-app** is served at
[{{{chat-app.routes.0.url}}}]({{{chat-app.routes.0.url}}})
Before we start to use it, let's inspect the app.
command:
filename: cf
args: [app, chat-app]
- body: |
You can see that 1 instance of the app is running. It has the default _1GB_ of memory and _1GB_ of disk. That is more than we need for a staticfile app. Let's scale things down. Memory should be _64M_ and disk _128M_.
This is called vertical scaling. Whenever scaling an app vertically Cloud Foundry has to restart it. This involves downtime. For now we're ok with that, so we add '-f'.
command:
filename: cf
args: [scale, chat-app, -m, 64M, -k, 128M, -f]
- body: |
Now let's open the application at
[{{{chat-app.routes.0.url}}}]({{{chat-app.routes.0.url}}})
You should see that the app "failed to load any messages". Oh dear! That's because its backend - the **message-service** - isn't running yet. But the **chat-app** is a good Cloud-citizen and handles issues with its downstream dependencies gracefully. That's an essential property of any cloud-native application.
Let's avert this misery and deploy the **message-service**. It's a Java Spring Boot web application that exposes two endpoints:
_GET /api/messages_ : returns the list of messages
_POST /api/messages_ : creates a new message
If it does not have a database attached it will run with an in-memory database. It is packaged into a JAR file located at _./builds/message-service_.
Again, we push by letting Cloud Foundry by pointing at the **message-service** JAR, picking a random route and configuring 650M of memory.
command:
filename: cf
args: [push, message-service, -p, ./builds/message-service.jar, --random-route, -m, 650M]
- body: |
The **message-service** is deployed and served at
[{{{message-service.routes.0.url}}}]({{{message-service.routes.0.url}}})
We have both the **chat-app** and the **message-service** deployed now. However, when we visit
[{{{chat-app.routes.0.url}}}]({{{chat-app.routes.0.url}}})
it still fails to load any messages. See for yourself.
Why is that?
In order to understand we must look at how traffic is currently routed.
command:
filename: cf
args: [routes]
- body: |
The **chat-app** is served at [{{{chat-app.routes.0.url}}}]({{{chat-app.routes.0.url}}})
The **message-service** is served at [{{{message-service.routes.0.url}}}]({{{message-service.routes.0.url}}})
The **chat-app** expects to reach the **message-service** at [{{{chat-app.routes.0.url}}}/api]({{{chat-app.routes.0.url}}}/api).
Mind the path _/api_. That's the problem!
Cloud Foundry's path-based routing to the rescue.
Let's map the route [{{{chat-app.routes.0.url}}}/api]({{{chat-app.routes.0.url}}}/api) to the **message-service**.
command:
filename: cf
args: [map-route, message-service, cfapps.io, --hostname, '{{chat-app.routes.0.hostname}}', --path, /api]
- body: |
Hooray! The error is gone. The **chat-app** says there are no messages.
It's time to take out your phone. Go to
[{{{chat-app.routes.0.url}}}]({{{chat-app.routes.0.url}}})
and chat away!
However, the more users there are the more load will our **message-service** have. At this moment we only have one instance of it running. We need more!
Adding more instances is called horizontal scaling. This does not require a restart and is almost instantaneous.
Let's scale out to 3. Planet scale!
command:
filename: cf
args: [scale, message-service, -i, 3]
- body: |
This is weird. As we're using the **chat-app** and sending messages, the message list keeps changing.
Why is that? As we haven't provided the **message-service** with a database, it is running with an in-memory database. That means each instance has its own state. And every time we post or get messages we hit another instance, hence the inconsistency.
This setup is violating the idea of 'stateless processes' according to the [twelve-factor app](https://12factor.net/processes).
Since Cloud Foundry might relocate instances in the cloud as it sees fit we might lose messages at any moment.
We need a database. Let's browse the marketplace.
command:
filename: cf
args: [marketplace]
- body: |
We can see there are plenty of services on offer, e.g.
* Redis
* Elasticsearch
* Sendgrid
* MongoDB
* app-autoscaler
* ...
Every service is available with different plans. Some are free, some incur cost. Let's ask Cloud Foundry to give us a database.
$(underline Elephantsql.com) offers Postgres as a service and is available in the marketplace. Let's find out more about its plans.
command:
filename: cf
args: [marketplace, -s, elephantsql]
- body: |
The smallest plan "_turtle_" provides 4 concurrent connections, 20MB Storage and is free. Just what we need!
Let's create an _elephantsql_ instance using the _turtle_ plan and name it "**database**".
command:
filename: cf
args: [create-service, elephantsql, turtle, database]
- body: |
The Postgres instance is ready.
We still need to connect it to the **message-service**. In Cloud Foundry terms this called binding.
When we bind a service to an app Cloud Foundry will provide all the necessary information to the app as environment variables. In this case it will provide a JDBC connection string to the **message-service**.
command:
filename: cf
args: [bind-service, message-service, database]
- body: |
But that's not all. We have to restart the **message-service**.
Since we're using Spring Boot it will will automatically pick up the database.
Caveat: In this case it is enough to just restart the application. In other cases we need to restage it for the changes to take effect (see [the docs](https://docs.cloudfoundry.org/devguide/deploy-apps/start-restart-restage.html)).
command:
filename: cf
args: [restart, message-service]
- ci: true
body: 'smoke test'
command:
filename: './scripts/smoke-test.sh'
args: ['{{{chat-app.routes.0.url}}}']
- body: |
As the instances of the **message-service** have restarted they are all using the database as a backing service. They no longer carry state. We can scale the message-service to our heart's content. The users of the chat will have a consistent experience, whether we have 1 or 100 instances running.
["May I present to you: the internet!"](https://youtu.be/iDbyYGrswtg)
But where's the proof? How can we tell that all incoming traffic is really distributed across all instances of the **message-service**?
Luckily, Cloud Foundry's _loggregator_ collects all application logs. It annotates every logged line with the type of process and app instance, e.g. [APP/PROC/WEB/2].
When inspecting the recent logs with a little help from grep we should see instances 0 - 2 logging equally often.
command:
filename: cf
args: [logs, --recent, message-service] # | grep GET | grep '\[APP\/PROC\/WEB\/\d\+\]''
- body: |
Once you're finished playing with the **chat-app**, let's clean up. If we don't want incur further cost against our PWS quota, we should decommission all apps and services.
The easiest way to achieve that is to delete the entire space.
command:
filename: cf
args: [delete-space, cfpush-tutorial, -f]
- body: |
🏁 That's all for now. 🏁
Expect updates to this tutorial. Thank you for coming this far!
Your feedback is valued. If there is anything I can do to improve your experience, please, let me know. Give the repository a star, open an issue or send a PR.
[github.com/mamachanko/cfpush](github.com/mamachanko/cfpush)
There's more [in the docs](https://docs.cloudfoundry.org/#read-the-docs).
Let's log you out. Bye bye!
command:
filename: cf
args: [logout]