Bí kíp để host apps hoàn toàn miễn phí

Nhắc đến dịch vụ web hosting, có rất nhiều lựa chọn tuyệt vời mà lại hoàn toàn free. Tuy vậy, có rất ít nơi cho phép bạn host full stack web app với APIs, CGI, hoặc AJAX backend queries – đặc biệt là khi bạn muốn làm gì khác hơn là chỉ PHP.

Bài viết này sẽ đơn giản là một bài hướng dẫn cặn kẽ cách bạn host scripts trên cloud server.

Khi nào thì dùng tới Cloud Application Platform

Cloud Application Platform là lựa chọn tuyệt vời khi bạn muốn viết code để chạy một server. Chúng thường cung cấp series của Linux application containers phục vụ cho việc triển khai code của bạn từ máy chủ local với một set command line keyword.

Heroku là một trong những dịch vụ hỗ trợ host code một cách dễ dàng. Với chính sách freemium model cho phép user sử dụng 500 giờ dịch vụ hoàn toàn miễn phí (Bảng giá chính thức).

Sau khi bạn đã viết xong code, bạn có thể execute commands để triển khai chúng và gửi vào một workspace của Heroku. Lúc đó, code sẽ được thi hành tùy vào trigger được đặt ra cho chúng. Trigger có thể là web page request, hoặc một quá trình xử lý thông tin nào đó.

Một điều khá hay là bạn sẽ không cần lo lắng về operating system (memory, storage, CPU, security patches) bởi chúng đều đã được quản lí giùm bạn – Tuy vậy nó đồng nghĩa với việc bạn sẽ bị giới hạn trong việc không thể chỉ định sử dụng resource một cách trực tiếp.

Sau đây là một số ví dụ điển hình về những tình huống mà Heroku trở nên vô cùng hữu ích:

  • Host website của chính bạn và tự viết nên web server
  • Thu thập dữ liệu từ một website và lưu trữ vào database để phân tích
  • Cung cấp một API server cho một task riêng biệt như weather data, lưu trữ Internet of Things sensor data, hoặc liên quan tới machine learning model.
  • Cung cấp dịch vụ database.

Cấu trúc của Heroku

Heroku cung cấp một virtual machine (VM) cho người dùng triển khai code. Tất nhiên là nếu bạn muốn dùng free thì sẽ chỉ được triển khai tới 5 application thôi (5 VM). Còn đối với ứng dụng thực của bạn, Heroku sẽ cung cấp một URL subdomain  riêng. Vì thế mà project name của bạn phải không bị trùng.

Những workspace có nhiều components khác nhau như: code và resource files (không phải dynamic data files), database (Postgres), và log files.

Trên local desktop của user, Heroku sẽ dùng directory name để xác định cho project của use, đồng thời cũng là để Heroku hiểu được nội dung của chúng. Do đó bạn có thể có nhiều project khác nhau với directory riêng biệt, miễn là khi chạy Heroku commands  thì nhớ để đúng folder.

Điều duy nhất bạn cần phải lưu ý đó là tất cả mọi thứ đều chạy từ memory. Không hề có bất cứ persistent storage nào. Tôi xin nhắc lại – bạn không thể lưu trữ bất cứ file nào trên file server. Cho persistence, Heroku cung cấp một postgress SQL database cho bạn lưu trữ theo ý thích.

Một ví dụ đơn giản – tạo ra tính năng phát hiện những thay đổi trên website

Nói cách khác nó sẽ y như trang www.changedetection.com; với một số component chính bạn cần phải có như:

  1. Một database dùng để lưu trữ: (a) địa chỉ email để notify sự thay đổi trên website; (b) website để track; (c) một bản ‘copy’ của website.
  2. Một đoạn code với tính năng kiểm tra ột website trong database của #1 (Python script)
  3. Một job scheduler để chạy #2
  4. Một  web user interface để add/delete website để theo dõi trong database của #1
  5. Một cơ chế gửi emails

Yêu cầu đối với bạn

  • Có GitHub account . Nếu chưa có thì đăng kí tại đây.
  • Có Heroku account. Nếu chưa có thì đăng kí tại đây.
  • Dùng Windows. Nhưng nếu không thì cũng chả sao tại tụi nó cũng từa tựa nhau.
  • Đã cài Python. Nếu chưa có thì đăng kí tại đây.
  • Có chút hiểu biết về Python. Nếu chưa có thì đăng kí tại đây.
  • Biết về SQL. Nếu chưa có thì đăng kí tại đây.

Tóm tắt về các bước trong bài viết

  • Bước 1: Tạo web user interface — Hello World trước tiên
  • Bước 2: Persistence — Tạo một database
  • Bước 3: Kiểm tra những thay đổi với website
  • Bước 4: Gửi email notification
  • Bước 5: List output trên web page
  • Bước 6: Triển khai

Bước 1: Tạo web user interface — Hello World trước tiên

Trước tiên, chung ta sẽ triển khai một ứng dụng đơn giản vào Heroku. Nó sẽ là tiền thân của web user interface (#4 trong cái component list). Để có thể cho ra một trang web, ta có thể dùng tới HTML page nhưng như vậy lại cần có web server. Nói cách khác, Khi bạn type vào URL của cái website, một program sẽ cần phải xử lí request và cung cấp nội dung cho HTML file. Bạn có thể tạo ra mini web server với Flask Python librar và cũng là điều mà chúng ta sẽ làm.

Tạo ra folder với directory name là webchecker  

Cài Flask library. Enter command: npm Flask.

Tạo ra Python program sau đây với và đặt tên là showchecks.py:

import os
from flask import Flask, request

app = Flask(__name__) #create an instance of the Flask library

@app.route('/hello') #whenever this webserver is called with <hostname:port>/hello then this section is called
def hello(): #The subroutine name that handles the call
	output = 'Hello World'
	return output #Whatever is returned from this subroutine is what is returned to the requester and is shown on the browser page

if __name__ == '__main__':
	port = int(os.environ.get('PORT', 5000)) #The port to be listening to — hence, the URL must be <hostname>:<port>/ inorder to send the request to this program
	app.run(host='0.0.0.0', port=port)  #Start listening

Trước khi bạn triển khai nó lên Heroku, hãy test nó bằng máy tính của mình. Bạn có thể dùng các bước sau:

Chạy phần mềm: python webchecker.com

Dùng trình duyệt web trên PC của bạn, vào: http://localhost:5000/hello

Sau khi đã chắc chắn thì bạn đã có thể triển khai nó lên Heroku. Trước đó, ta cũng sẽ cần cung cấp một số file để hỗ trợ Heroku hiểu rõ thêm về ứng dụng của bạn.

Đầu tiên là, requirements.txt

Flask==0.12

Một file giúp Heroku biết nên chạy cái gì khi webrequest được thực hiện

web: python showchecks.py

Cuối cùng là phiên bản runtime của Python

python-3.6.1

Như vậy ta sẽ có tổng cộng 4 file:

  1. showchecker.py vốn là code của bạn
  2. requirements.txt cho list của non-standard library dependencies. Dùng để add những libraries mới mà lại không phải là Python Standard Library (phải dùng Tool như “PIP” để cài đặt)
  3. Procfile, vốn là Python script để chạy khi website được call – Nhớ là phải update nó nếu bạn thay đổi Python file.
  4. runtime.txt, chính là phiên bản thật của Python để dùng

Bạn có thể triển khai trong command line theo những bước sau:

  1. heroku create webchecker01 — buildpack heroku/python
  2. git add *.* *
  3. git status
  4. git commit -m “all files”
  5. git push heroku master

Với command #1 (heroku create…), phần “webechecker01” là một unique name mà bạn cần cung cấp cho name của app.

Command #3 (git status) sẽ cho biết file nào sẵn sàng để triển khai. Hãy luôn chắc chắn là tất cả file đều có mặt đầy đủ, nếu chưa thì add thêm chúng vào bằng git add <filename>.

Và giờ bạn đã có thể check website của mình: <application name>.herokuapp.com/hello

À mà cũng nên bảo đảm rằng bạn vẫn có thể xem được logs nhé. Xem bằng chạy command

heroku logstrong webchecker directory.

Nếu mọi thứ không diễn ra như ý muốn thì bạn sẽ phải dừng và kiểm tra lại. Hãy dùng https://dashboard.heroku.com để biết rõ hơn nguyên nhân.

Step 2: Persistence — Tạo một database

Để có thể tạo ra thêm nhiều phần mềm hữu ích khác bạn sẽ cần tới data store. Đây chính là lúc dịch vụ Postgres database  tỏa sáng. Bạn sẽ cần phải triển khai dịch vụ của Heroku database, tạo ra table rồi mới có thể kết nối code của bạn đến với database (để test)

Để có thể triển khai một database service thì trước hết ta cần phải tạo ra nó bằng command sau: 

heroku addons:create heroku-postgresql:hobby-dev

Tiếp theo, truy cập vào database từ command line và tạo ra table của riêng bạn. Lưu ý là database sẽ được xây trên dịch vụ đám mây của Heroku. Tuy nhiên bạn vẫn có vào được nhờ vào command line. Để log vào database  thông qua console, chạy command heroku pg:psql. Nhưng phải nhớ là bạn bắt buộc làm nó trong webchecker folder thì Heroku mới hiểu là database này dành cho webchecker site.

Dùng command `/d` để xem list của tables type

Nhằm tạo ra một table, bạn sẽ phải dùng SQL statement bình thường. Và cho webchecker program, chúng ta sẽ làm một table với 4 cột như sau:

  • ID – Tự động tạo cho mỗi entry (dùng type “serial”)
  • Website – website để theo dõi
  • Emailaddress – địa chỉ email để gửi notification khi có thay đổi diễn ra
  • Lasthashcode- chúng ta sẽ không lưu trữ nguyên cả bản copy của webpage mà thay vào đó sẽ tạo ra hash dựa trên HTML của webpage và so sánh chúng. Nó sẽ giúp tiết kiệm lượng thông tin lưu trữ nhưng sẽ không phản ánh hết những thay đổi đã xảy ra.
  • Lastchangedate  – thời điểm mà sự thay đổi diễn ra.

Để tạo table như trên, ta sẽ điền command vào trong Heroku Postgres database console:

CREATE TABLE webcheckerdb (id serial, website varchar(250), emailaddress varchar(250), lasthashcode varchar(32), lastchangedate timestamp DEFAULT current_date );

Tiếp theo, ta thêm một record vào database để giúp bảo đảm web UI chạy tốt

INSERT into webcheckerdb values(DEFAULT, 'news.google.com', '[email protected]', '', DEFAULT);

Giờ bạn có thể thoát với \q.

Bước 3: check website để theo dõi sự thay đổi

Trước tiên, ta hãy dùng một đoạn code để xem site có thể retrieved lại được không.

Như vậy, nhiệm vụ được đề ra là retrieve một webpage. Bạn sẽ cần tạo ra một Python file với tên gọi là checkwebsite.py:

import http.client
import hashlib
import pprint


def getCurrentWebsiteHash(weburl):
	httpConn = http.client.HTTPSConnection(weburl)
	httpConn.request('GET', weburl)

	resp = httpConn.getresponse()
	data = resp.read()

	hash_object = hashlib.md5( data )
	print(hash_object.hexdigest())

	return hash_object.hexdigest()

def getWebList():
	webRecordList = [ {'website':'www.google.com', 'lasthashcode':'xxx'} ]
	return webRecordList

def checkWebList(weblist):
	for webrecord in weblist:
		pprint.pprint(webrecord)
		currWebHash = getCurrentWebsiteHash( webrecord['website'])
		if currWebHash != webrecord['lasthashcode']:
			print( 'Website ' + webrecord['website'] + ' has changed ')

if __name__ == "__main__":
	weblist = getWebList()
	checkWebList( weblist )

Kết quả ta có được là:

Nếu bạn gặp phải vấn đề bị thiếu libraries thì thêm chúng vào bằng command line pip install <library>

Giờ ta sẽ kết nối chúng tới database với những dòng code:

import http.client
import hashlib
import psycopg2
from psycopg2.extras import RealDictCursor
import traceback
import urllib.parse
import pprint
import os

urllib.parse.uses_netloc.append("postgres")
url = urllib.parse.urlparse(os.environ["DATABASE_URL"])
dbConn = psycopg2.connect( database=url.path[1:], user=url.username, password=url.password, host=url.hostname, port=url.port)
dbCur = dbConn.cursor(cursor_factory=RealDictCursor)

def getCurrentWebsiteHash(weburl):
	print("getting url:"+ weburl)
	httpConn = http.client.HTTPSConnection(weburl)  #Create connection object
	httpConn.request('GET', weburl)	#Get the website

	resp = httpConn.getresponse()	
	data = resp.read()				#Get the webdata into a string object

	hash_object = hashlib.md5( data )	#Createa  hash code
	print(hash_object.hexdigest())		#print hash code

	return hash_object.hexdigest()

def getWebList():
	rows = []
	try:
		dbCur.execute("select * from webcheckerdb" )	#Get all records from database
		rows = dbCur.fetchall()
	except:
		print ("error during select: " + str(traceback.format_exc()) )
	return rows

def checkWebList(weblist):
	for webrecord in weblist:	#Loop through each record in database
		pprint.pprint(webrecord)
		currWebHash = getCurrentWebsiteHash( webrecord['website'])	#Get the current websites latest hash code
		if currWebHash != webrecord['lasthashcode']:				
			print( 'Website ' + webrecord['website'] + ' has changed ')
			try:
				#If there is a change, print out the change, but also update the database so that next time
				#the message wont get triggered again
				dbCur.execute("update webcheckerdb set lasthashcode ='" + str(currWebHash) + "' where id = '" + str(webrecord['id']) +"'" )
				dbConn.commit()
				print( 'Website hash updated for next time')
			except:
				print ("error during update: " + str(traceback.format_exc()) )

if __name__ == "__main__":
	weblist = getWebList()
	checkWebList( weblist )

Tuy vậy, nếu thử chạy chúng bạn sẽ thấy xuất hiện lỗi ngay – `KeyError: ‘DATABASE_URL’`. Đó là vì Python code của bạn đang tìm địa chỉ của web trên Postgres database host bởi Heroku. Sau này thì việc này sẽ được tự động update cho bạn trên server nhưng trên máy tính của bạn thì phải tự cập nhật bằng tay theo 2 bước sau:

  1. Vào heroku config
  2. Chỉnh DATABASE_URL=<the database string listed from “heroku config”>

Bước 4: Gửi email notification thông báo về thay đổi

Bước cuối cùng là gửi email. Để làm được diều này, bạn sẽ phải cài thêm một Addon với tính năng gửi mail – bạn có thể kiếm chúng trên marketplace của Heroku: https://elements.heroku.com/addons

Trong bài viết này, ta sẽ dùng Addon gọi là SendGrid: https://elements.heroku.com/addons/sendgrid

Bạn có thể thêm nó vào app của mình bằng command line:

heroku addons:create sendgrid:starter

Trước khi sử dụng, bạn cũng sẽ cần tạo một API key, Double click trên SendGrid component và vào Settings->API Key->Create Key.

Một khi bạn đã có key, copy và paste nó vào command prompt:

heroku config:set SENDGRID_API_KEY=<API key from above>

Tuy nhiên nó chỉ mới được register trên server thôi, bạn cũng cân phải làm bằng tay cho desktop của mình:

set SENDGRID_API_KEY=<API Key from above again>

Cuối cùng bạn đã có thể test thử trong Python script gọi là sendmail.py. Để cài library đó, dùng pip install sendgrid:

import sendgrid
import os
from sendgrid.helpers.mail import *


def sendemail(recipient, emailSubject, body):
	sg = sendgrid.SendGridAPIClient(apikey=os.environ.get('SENDGRID_API_KEY'))
	from_email = Email("[email protected]")
	to_email = Email(recipient)
	content = Content("text/plain", body)
	mail = Mail(from_email, emailSubject, to_email, content)
	response = sg.client.mail.send.post(request_body=mail.get())

	print("### Email sent to: "+ recipient + " ###")
	print(response.status_code)
	print(response.body)
	print(response.headers)

	

if __name__ == "__main__":
	sendemail('[email protected]', "hello world", "you have mail!")

Để kiểm tra xem mail được gởi hay chưa, bạn hãy vào lại SendGrid dashboard và kiểm tra mục Statistics Overview :

Nhớ là lúc check mail thì bạn cũng phải check spam luôn nhé.

Một khi mọi thứ đều hoạt động đúng theo kế hoạch thì bạn giờ chỉ cần thêm 2 dòng code nữa vào checkwebsite.py script thôi:

import sendmail #import the send email subroutine you wrote above
...
#call the subroutine after find the hashcode has changed
sendmail.sendemail(webrecord['emailaddress'], 'Website changed', webrecord['website'] + ' changed')
import http.client
import hashlib
import psycopg2
from psycopg2.extras import RealDictCursor
import traceback
import urllib.parse
import pprint
import os
import sendmail  #import the send email subroutine 

urllib.parse.uses_netloc.append("postgres")
url = urllib.parse.urlparse(os.environ["DATABASE_URL"])
dbConn = psycopg2.connect( database=url.path[1:], user=url.username, password=url.password, host=url.hostname, port=url.port)
dbCur = dbConn.cursor(cursor_factory=RealDictCursor)

def getCurrentWebsiteHash(weburl):
	print("getting url:"+ weburl)
	httpConn = http.client.HTTPSConnection(weburl)  #Create connection object
	httpConn.request('GET', weburl)	#Get the website

	resp = httpConn.getresponse()	
	data = resp.read()				#Get the webdata into a string object

	hash_object = hashlib.md5( data )	#Createa  hash code
	print(hash_object.hexdigest())		#print hash code

	return hash_object.hexdigest()

def getWebList():
	rows = []
	try:
		dbCur.execute("select * from webcheckerdb" )	#Get all records from database
		rows = dbCur.fetchall()
	except:
		print ("error during select: " + str(traceback.format_exc()) )
	return rows

def checkWebList(weblist):
	for webrecord in weblist:	#Loop through each record in database
		pprint.pprint(webrecord)
		currWebHash = getCurrentWebsiteHash( webrecord['website'])	#Get the current websites latest hash code
		if currWebHash != webrecord['lasthashcode']:		
			print( 'Website ' + webrecord['website'] + ' has changed email to ' + webrecord['emailaddress'])
			#call the subroutine after find the hashcode has changed
			sendmail.sendemail(webrecord['emailaddress'], 'Website changed',  webrecord['website'] + ' changed')
			try:
				#If there is a change, print out the change, but also update the database so that next time
				#the message wont get triggered again
				dbCur.execute("update webcheckerdb set lasthashcode ='" + str(currWebHash) + "' where id = '" + str(webrecord['id']) +"'" )
				dbConn.commit()
				print( 'Website hash updated for next time')
			except:
				print ("error during update: " + str(traceback.format_exc()) )

if __name__ == "__main__":
	weblist = getWebList()
	checkWebList( weblist )

Bước 5: Lập list cho output trên web page và set thời gian cho job

Giờ chúng ta cần liệt kê ra các output trên webpage. Bạn sẽ cần tới querying database rồi cycling và hiển thị chúng. Lấy ví dụ là ‘Hello World’ code ở trên, nó sẽ thứ hiện quá trình modification. Bạn có thể test nó với path mà tôi đã tạo ra là: http://localhost:5000/list

import os
from flask import Flask, request
import hashlib
import psycopg2
from psycopg2.extras import RealDictCursor
import traceback
import urllib.parse
import pprint 

app = Flask(__name__) #create an instance of the Flask library
urllib.parse.uses_netloc.append("postgres")
url = urllib.parse.urlparse(os.environ["DATABASE_URL"])
dbConn = psycopg2.connect( database=url.path[1:], user=url.username, password=url.password, host=url.hostname, port=url.port)
dbCur = dbConn.cursor(cursor_factory=RealDictCursor)


@app.route('/list') #whenever this webserver is called with <hostname:port>/hello then this section is called
def list(): #The subroutine name that handles the call
	output = 'Check status:'
	rows = []
	try:
		dbCur.execute("select * from webcheckerdb" )	#Get all records from database
		rows = dbCur.fetchall()
		for webrecord in rows:	#Loop through each record in database
			output = output + '<BR> ' + pprint.pformat(webrecord)
	except:
		output = "error during select: " + str(traceback.format_exc())

	return output #Whatever is returned from this subroutine is what is returned to the requester and is shown on the browser page


@app.route('/hello') #whenever this webserver is called with <hostname:port>/hello then this section is called
def hello(): #The subroutine name that handles the call
	output = 'Hello World'
	return output #Whatever is returned from this subroutine is what is returned to the requester and is shown on the browser page

if __name__ == '__main__':
	port = int(os.environ.get('PORT', 5000)) #The port to be listening to — hence, the URL must be <hostname>:<port>/ inorder to send the request to this program
	app.run(host='0.0.0.0', port=port)  #Start listening

Bước 6: Triển khai

Bước cuối cùng là triển khai tất cả mọi thứ lên Heroku  và lên schedule để nó check mail.

Đến bước này, bạn sẽ phải có những file sau:

  1. Procfile —  file hướng tới showchecker.py
  2. requirements.txt — file chứa library dependencies
  3. runtime.txt — phiên bản của python
  4. showchecker.py —python code hiển thị database output trên web thông qua <your appname>.herokuapp.com/list
  5. checkwebsite.py —python code kiểm tra những thay đổi trên website.

Đối với requirements.txt bạn sẽ cần update các bản libraries mới nhất

Flask==0.12
psycopg2==2.6.2
sendgrid==3.6.3

Triển khai tất cả lên Heroku:

  1. git add *.* *
  2. git commit -m “deployment”
  3. git push heroku master

Test từng component:

  1. Vào <your app name>.herokuapp.com/hello
  2. Vào <your app name>.herokuapp.com/list

Nếu có bất cứ errors nào thì chạy heroku logs trong command line để xem có vấn đề gì rồi chạy checkwebsite.py  trực tiếp trên Heroku để bảo đảm không còn bị lỗi gì khác.

Để làm điều đó, bạn chỉ cần type:

heroku run python checkwebsite.py

Và giờ thì ta đã có thể lên schedule được rùi (tất nhiên là cần phải có Addon)

heroku addons:create scheduler:standard

Chúc mừng bạn đã thành công trong việc làm ra một ứng dụng với chức năng gửi mail nhắc mỗi khi có thay đổi diễn ra trên web page, bạn có thể dùng command line `python checkwebsite.py` để chạy chương trình.

Nguồn: blog.topdev.vn via Medium