用tsuru部署Python应用

概述

本文档是在tsuru中部署一个简单的Python应用的实战指南。例子应用是一个关联MySQL服务的很简单的Django项目。这些例子也适用于任何WSGI应用。

在tsuru中创建应用

使用app-create命令创建应用:

  1. $ tsuru app-create <app-name> <app-platform>

对于Python来说,应用平台是,你猜,python!让我们脑洞打开,开发一个从未有人做过的教材应用:一个博客,而且它的名字也应该是很有创意,让我们叫他blog

  1. $ tsuru app-create blog python

使用platform-list命令列出所有的可用的平台。使用app-list查看你所有的应用。

  1. $ tsuru app-list
  2. +-------------+-------------------------+--------------------------+
  3. | Application | Units State Summary | Address |
  4. +-------------+-------------------------+--------------------------+
  5. | blog | 0 of 0 units in-service | blog.192.168.50.4.nip.io |
  6. +-------------+-------------------------+--------------------------+

然后就可以发送你的应用代码了。

应用的代码

本文档不会专注于介绍如何用Django实现博客,你可以从GitHub clone整个源代码:https://github.com/tsuru/tsuru-django-sample。 下面是我们在项目中所做的事情:

. 创建项目 (django-admin.py startproject). 启用django-admin. 安装South. 创建一个"posts"应用(django-admin.py startapp posts). 为应用添加"Post"model. 在django-admin中注册该model. 用South进行数据库迁移

通过Git部署

在创建新的应用时,tsuru会显示应该使用的Git远程分支。用app-info命令可以获得其信息:

  1. $ tsuru app-info --app blog
  2. Application: blog
  3. Repository: git@192.168.50.4.nip.io:blog.git
  4. Platform: python
  5. Teams: admin
  6. Address: blog.192.168.50.4.nip.io
  7. Owner: admin@example.com
  8. Team owner: admin
  9. Deploys: 0
  10. Pool: theonepool
  11. App Plan:
  12. +---------------+--------+------+-----------+--------+---------+
  13. | Name | Memory | Swap | Cpu Share | Router | Default |
  14. +---------------+--------+------+-----------+--------+---------+
  15. | autogenerated | 0 MB | 0 MB | 100 | | false |
  16. +---------------+--------+------+-----------+--------+---------+

Git远程分支被用来通过Git部署应用。当修改被推送到远程分支时,项目同时也被部署:

  1. $ git push git@192.168.50.4.nip.io:blog.git master
  2. Counting objects: 119, done.
  3. Delta compression using up to 4 threads.
  4. Compressing objects: 100% (53/53), done.
  5. Writing objects: 100% (119/119), 16.24 KiB, done.
  6. Total 119 (delta 55), reused 119 (delta 55)
  7. remote:
  8. remote: ---> tsuru receiving push
  9. remote:
  10. remote: From git://cloud.tsuru.io/blog.git
  11. remote: * branch master -> FETCH_HEAD
  12. remote:
  13. remote: ---> Installing dependencies
  14. #####################################
  15. # OMIT (see below) #
  16. #####################################
  17. remote: ---> Restarting your app
  18. remote:
  19. remote: ---> Deploy done!
  20. remote:
  21. To git@192.168.50.4.nip.io:blog.git
  22. a211fba..bbf5b53 master -> master

如果遇到"Permission denied (publickey)."的错误,请确保你是团队一员并把公钥加到tsuru中。用key-add命令添加公钥:

  1. $ tsuru key-add mykey ~/.ssh/id_rsa.pub

使用git remote add命令来避免每次push代码时都要输入整个远程仓库的链接:

  1. $ git remote add tsuru git@192.168.50.4.nip.io:blog.git

然后运行:

  1. $ git push tsuru master
  2. Everything up-to-date

从此之后就可以省略掉—app标记:

  1. $ tsuru app-info
  2. Application: blog
  3. Repository: git@192.168.50.4.nip.io:blog.git
  4. Platform: python
  5. Teams: admin
  6. Address: blog.192.168.50.4.nip.io
  7. Owner: admin@example.com
  8. Team owner: admin
  9. Deploys: 0
  10. Pool: theonepool
  11. Units: 1
  12. +------------+---------+
  13. | Unit | State |
  14. +------------+---------+
  15. | eab5151eff | started |
  16. +------------+---------+
  17. App Plan:
  18. +---------------+--------+------+-----------+--------+---------+
  19. | Name | Memory | Swap | Cpu Share | Router | Default |
  20. +---------------+--------+------+-----------+--------+---------+
  21. | autogenerated | 0 MB | 0 MB | 100 | | false |
  22. +---------------+--------+------+-----------+--------+---------+

列出依赖

在上一个部分我们忽略了部署过程的依赖。在tsuru中,一个应用可以有两种依赖:

  • 操作系统级别的依赖, 以底层操作系统使用的包管理器为代表(比如: yumapt-get);
  • 平台依赖, 以平台/语言依赖的包管理工具为代表(在Python中是pip)。
    所有的apt-get依赖必须在requirements.apt文件中指定,放在应用的根目录,同理,pip的依赖也必须放在应用根目录名为requirements.txt的文件中。因为要用Django连接MySQL,我们需要用pip安装mysql-python,这个包依赖两个apt-get包:python-devlibmysqlclient-dev,因此requirements.apt内容如下:
  1. libmysqlclient-dev
  2. python-dev

下面是requirements.txt的内容:

  1. Django==1.4.1
  2. MySQL-python==1.2.3
  3. South==0.7.6

请注意文件中也包含了South,用于数据库移植,对于Django应用,这是很常见的。下面是安装这些依赖的完整输出:

  1. % git push tsuru master
  2. #####################################
  3. # OMIT #
  4. #####################################
  5. remote: Reading package lists...
  6. remote: Building dependency tree...
  7. remote: Reading state information...
  8. remote: python-dev is already the newest version.
  9. remote: The following extra packages will be installed:
  10. remote: libmysqlclient18 mysql-common
  11. remote: The following NEW packages will be installed:
  12. remote: libmysqlclient-dev libmysqlclient18 mysql-common
  13. remote: 0 upgraded, 3 newly installed, 0 to remove and 0 not upgraded.
  14. remote: Need to get 2360 kB of archives.
  15. remote: After this operation, 9289 kB of additional disk space will be used.
  16. remote: Get:1 http://archive.ubuntu.com/ubuntu/ quantal/main mysql-common all 5.5.27-0ubuntu2 [13.7 kB]
  17. remote: Get:2 http://archive.ubuntu.com/ubuntu/ quantal/main libmysqlclient18 amd64 5.5.27-0ubuntu2 [949 kB]
  18. remote: Get:3 http://archive.ubuntu.com/ubuntu/ quantal/main libmysqlclient-dev amd64 5.5.27-0ubuntu2 [1398 kB]
  19. remote: debconf: unable to initialize frontend: Dialog
  20. remote: debconf: (Dialog frontend will not work on a dumb terminal, an emacs shell buffer, or without a controlling terminal.)
  21. remote: debconf: falling back to frontend: Readline
  22. remote: debconf: unable to initialize frontend: Readline
  23. remote: debconf: (This frontend requires a controlling tty.)
  24. remote: debconf: falling back to frontend: Teletype
  25. remote: dpkg-preconfigure: unable to re-open stdin:
  26. remote: Fetched 2360 kB in 1s (1285 kB/s)
  27. remote: Selecting previously unselected package mysql-common.
  28. remote: (Reading database ... 23143 files and directories currently installed.)
  29. remote: Unpacking mysql-common (from .../mysql-common_5.5.27-0ubuntu2_all.deb) ...
  30. remote: Selecting previously unselected package libmysqlclient18:amd64.
  31. remote: Unpacking libmysqlclient18:amd64 (from .../libmysqlclient18_5.5.27-0ubuntu2_amd64.deb) ...
  32. remote: Selecting previously unselected package libmysqlclient-dev.
  33. remote: Unpacking libmysqlclient-dev (from .../libmysqlclient-dev_5.5.27-0ubuntu2_amd64.deb) ...
  34. remote: Setting up mysql-common (5.5.27-0ubuntu2) ...
  35. remote: Setting up libmysqlclient18:amd64 (5.5.27-0ubuntu2) ...
  36. remote: Setting up libmysqlclient-dev (5.5.27-0ubuntu2) ...
  37. remote: Processing triggers for libc-bin ...
  38. remote: ldconfig deferred processing now taking place
  39. remote: sudo: Downloading/unpacking Django==1.4.1 (from -r /home/application/current/requirements.txt (line 1))
  40. remote: Running setup.py egg_info for package Django
  41. remote:
  42. remote: Downloading/unpacking MySQL-python==1.2.3 (from -r /home/application/current/requirements.txt (line 2))
  43. remote: Running setup.py egg_info for package MySQL-python
  44. remote:
  45. remote: warning: no files found matching 'MANIFEST'
  46. remote: warning: no files found matching 'ChangeLog'
  47. remote: warning: no files found matching 'GPL'
  48. remote: Downloading/unpacking South==0.7.6 (from -r /home/application/current/requirements.txt (line 3))
  49. remote: Running setup.py egg_info for package South
  50. remote:
  51. remote: Installing collected packages: Django, MySQL-python, South
  52. remote: Running setup.py install for Django
  53. remote: changing mode of build/scripts-2.7/django-admin.py from 644 to 755
  54. remote:
  55. remote: changing mode of /usr/local/bin/django-admin.py to 755
  56. remote: Running setup.py install for MySQL-python
  57. remote: building '_mysql' extension
  58. remote: gcc -pthread -fno-strict-aliasing -DNDEBUG -g -fwrapv -O2 -Wall -Wstrict-prototypes -fPIC -Dversion_info=(1,2,3,'final',0) -D__version__=1.2.3 -I/usr/include/mysql -I/usr/include/python2.7 -c _mysql.c -o build/temp.linux-x86_64-2.7/_mysql.o -DBIG_JOINS=1 -fno-strict-aliasing -g
  59. remote: In file included from _mysql.c:36:0:
  60. remote: /usr/include/mysql/my_config.h:422:0: warning: "HAVE_WCSCOLL" redefined [enabled by default]
  61. remote: In file included from /usr/include/python2.7/Python.h:8:0,
  62. remote: from pymemcompat.h:10,
  63. remote: from _mysql.c:29:
  64. remote: /usr/include/python2.7/pyconfig.h:890:0: note: this is the location of the previous definition
  65. remote: gcc -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions -Wl,-Bsymbolic-functions -Wl,-z,relro build/temp.linux-x86_64-2.7/_mysql.o -L/usr/lib/x86_64-linux-gnu -lmysqlclient_r -lpthread -lz -lm -lrt -ldl -o build/lib.linux-x86_64-2.7/_mysql.so
  66. remote:
  67. remote: warning: no files found matching 'MANIFEST'
  68. remote: warning: no files found matching 'ChangeLog'
  69. remote: warning: no files found matching 'GPL'
  70. remote: Running setup.py install for South
  71. remote:
  72. remote: Successfully installed Django MySQL-python South
  73. remote: Cleaning up...
  74. #####################################
  75. # OMIT #
  76. #####################################
  77. To git@192.168.50.4.nip.io:blog.git
  78. a211fba..bbf5b53 master -> master

运行应用

如你所见,部署的输出信息中有一个步骤叫做"重启你的应用"。在这个步骤中,如果应用在运行,tsuru会重启它,如果没有运行,tsuru会启动它。但是tsuru是如何启动一个应用呢?非常简单,它使用了一个Procfile(从Foreman中偷学的概念)。这个Procfile用于描述应用如何被启动。我们可以使用gunicorn去启动Django应用。如下是Procfile的内容:

  1. web: gunicorn -b 0.0.0.0:$PORT blog.wsgi

现在我们提交文件并将其推送到tsuru的git服务器,再运行一次部署任务:

  1. $ git add Procfile
  2. $ git commit -m "Procfile: added file"
  3. $ git push tsuru master
  4. Counting objects: 5, done.
  5. Delta compression using up to 4 threads.
  6. Compressing objects: 100% (2/2), done.
  7. Writing objects: 100% (3/3), 326 bytes, done.
  8. Total 3 (delta 1), reused 0 (delta 0)
  9. remote:
  10. remote: ---> tsuru receiving push
  11. remote:
  12. remote: ---> Installing dependencies
  13. remote: Reading package lists...
  14. remote: Building dependency tree...
  15. remote: Reading state information...
  16. remote: python-dev is already the newest version.
  17. remote: libmysqlclient-dev is already the newest version.
  18. remote: 0 upgraded, 0 newly installed, 0 to remove and 1 not upgraded.
  19. remote: Requirement already satisfied (use --upgrade to upgrade): Django==1.4.1 in /usr/local/lib/python2.7/dist-packages (from -r /home/application/current/requirements.txt (line 1))
  20. remote: Requirement already satisfied (use --upgrade to upgrade): MySQL-python==1.2.3 in /usr/local/lib/python2.7/dist-packages (from -r /home/application/current/requirements.txt (line 2))
  21. remote: Requirement already satisfied (use --upgrade to upgrade): South==0.7.6 in /usr/local/lib/python2.7/dist-packages (from -r /home/application/current/requirements.txt (line 3))
  22. remote: Cleaning up...
  23. remote:
  24. remote: ---> Restarting your app
  25. remote: /var/lib/tsuru/hooks/start: line 13: gunicorn: command not found
  26. remote:
  27. remote: ---> Deploy done!
  28. remote:
  29. To git@192.168.50.4.nip.io:blog.git
  30. 81e884e..530c528 master -> master

现在我们碰到了一个错误:gunicorn: command not found。这意味需要在requirements.txt中添加gunicorn的依赖:

  1. $ cat >> requirements.txt
  2. gunicorn==0.14.6
  3. ^D

提交修改,再次运行部署:

  1. $ git add requirements.txt
  2. $ git commit -m "requirements.txt: added gunicorn"
  3. $ git push tsuru master
  4. Counting objects: 5, done.
  5. Delta compression using up to 4 threads.
  6. Compressing objects: 100% (3/3), done.
  7. Writing objects: 100% (3/3), 325 bytes, done.
  8. Total 3 (delta 1), reused 0 (delta 0)
  9. remote:
  10. remote: ---> tsuru receiving push
  11. remote:
  12. [...]
  13. remote: ---> Restarting your app
  14. remote:
  15. remote: ---> Deploy done!
  16. remote:
  17. To git@192.168.50.4.nip.io:blog.git
  18. 530c528..542403a master -> master

应用部署成功,可以通过app-list命令获得IP或者主机名,然后用浏览器去访问。比如,在下面的列表中:

  1. $ tsuru app-list
  2. +-------------+-------------------------+---------------------+
  3. | Application | Units State Summary | Address |
  4. +-------------+-------------------------+---------------------+
  5. | blog | 1 of 1 units in-service | blog.cloud.tsuru.io |
  6. +-------------+-------------------------+---------------------+

可以通过http://blog.cloud.tsuru.io/admin/ 访问应用的管理界面。

使用服务

既然gunicorn在运行,我们就能通过浏览器访问应用,此时会碰到一个Django的错误:"Can't connect to local MySQL server through socket'/var/run/mysqld/mysqld.sock' (2)"。这个错误的意思是我们不能在本地访问MySQL。这是因为我们不应该在本地连接MySQL,必须要使用服务。服务的工作流分为下面两步:

. 创建一个服务的实例. 将服务的实例绑定到应用但是如何知道哪些服务是可用的呢?这很简单,运行service-list命令:

  1. $ tsuru service-list
  2. +----------------+-----------+
  3. | Services | Instances |
  4. +----------------+-----------+
  5. | elastic-search | |
  6. | mysql | |
  7. +----------------+-----------+

上面service-list的输出说明有两个可用的服务:"elastic-search"和"mysql",并且没有实例。通过service-add命令可以创建MySQL实例:

  1. $ tsuru service-add mysql blogsql
  2. Service successfully added.

现在,如果再次运行service-list命令,可以从输出中看到新的服务实例:

  1. $ tsuru service-list
  2. +----------------+-----------+
  3. | Services | Instances |
  4. +----------------+-----------+
  5. | elastic-search | |
  6. | mysql | blogsql |
  7. +----------------+-----------+

使用service-bind命令将服务实例绑定到应用:

  1. $ tsuru service-bind mysql blogsql
  2. Instance blogsql is now bound to the app blog.
  3. The following environment variables are now available for use in your app:
  4. - MYSQL_PORT
  5. - MYSQL_PASSWORD
  6. - MYSQL_USER
  7. - MYSQL_HOST
  8. - MYSQL_DATABASE_NAME
  9. For more details, please check the documentation for the service, using service-doc command.

从绑定的输出中我们看出,可以通过环境变量来连接MySQL服务器。下一步就是更新settings.py去使用这些环境变量去连接数据库:

  1. import os
  2. DATABASES = {
  3. 'default': {
  4. 'ENGINE': 'django.db.backends.mysql',
  5. 'NAME': os.environ.get('MYSQL_DATABASE_NAME', 'blog'),
  6. 'USER': os.environ.get('MYSQL_USER', 'root'),
  7. 'PASSWORD': os.environ.get('MYSQL_PASSWORD', ''),
  8. 'HOST': os.environ.get('MYSQL_HOST', ''),
  9. 'PORT': os.environ.get('MYSQL_PORT', ''),
  10. }
  11. }

现在提交修改,再次运行部署:

  1. $ git add blog/settings.py
  2. $ git commit -m "settings: using environment variables to connect to MySQL"
  3. $ git push tsuru master
  4. Counting objects: 7, done.
  5. Delta compression using up to 4 threads.
  6. Compressing objects: 100% (4/4), done.
  7. Writing objects: 100% (4/4), 535 bytes, done.
  8. Total 4 (delta 3), reused 0 (delta 0)
  9. remote:
  10. remote: ---> tsuru receiving push
  11. remote:
  12. remote: ---> Installing dependencies
  13. #####################################
  14. # OMIT #
  15. #####################################
  16. remote:
  17. remote: ---> Restarting your app
  18. remote:
  19. remote: ---> Deploy done!
  20. remote:
  21. To git@192.168.50.4.nip.io:blog.git
  22. ab4e706..a780de9 master -> master

如果再次尝试访问管理页面,会碰到另一个错误:"Table 'blogsql.django_session' doesn't exist"。嗯,这意味着我们已经连接到了数据库,绑定起作用了,但是还没有建立数据库。需要在远程服务器运行syncdbmigrate(如果使用South的话)。使用app-run命令:

  1. $ tsuru app-run -- python manage.py syncdb --noinput
  2. Syncing...
  3. Creating tables ...
  4. Creating table auth_permission
  5. Creating table auth_group_permissions
  6. Creating table auth_group
  7. Creating table auth_user_user_permissions
  8. Creating table auth_user_groups
  9. Creating table auth_user
  10. Creating table django_content_type
  11. Creating table django_session
  12. Creating table django_site
  13. Creating table django_admin_log
  14. Creating table south_migrationhistory
  15. Installing custom SQL ...
  16. Installing indexes ...
  17. Installed 0 object(s) from 0 fixture(s)
  18. Synced:
  19. > django.contrib.auth
  20. > django.contrib.contenttypes
  21. > django.contrib.sessions
  22. > django.contrib.sites
  23. > django.contrib.messages
  24. > django.contrib.staticfiles
  25. > django.contrib.admin
  26. > south
  27. Not synced (use migrations):
  28. - blog.posts
  29. (use ./manage.py migrate to migrate these)

migrate的使用类似.

部署钩子

每次部署后手动运行syncdb或者migrate会很烦人。所以我们可以配置一个自动的钩子,在每次应用重启前或者重启后运行。tsuru会解析一个名为tsuru.yaml的文件,并重启的钩子。如扩展所建议的那样,这是一个YAML文件,包含了一系列在重启前后要运行的命令。下面是一个tsuru.yaml文件的例子:

  1. hooks:
  2. build:
  3. - python manage.py syncdb --noinput
  4. - python manage.py migrate

更多内容,请查看tsuru.yaml中的钩子文档部分。tsuru会在项目的根目录查找这个文件。让我们提交并且部署它:

  1. $ git add tsuru.yaml
  2. $ git commit -m "tsuru.yaml: added file"
  3. $ git push tsuru master
  4. Counting objects: 4, done.
  5. Delta compression using up to 4 threads.
  6. Compressing objects: 100% (3/3), done.
  7. Writing objects: 100% (3/3), 338 bytes, done.
  8. Total 3 (delta 1), reused 0 (delta 0)
  9. remote:
  10. remote: ---> tsuru receiving push
  11. remote:
  12. remote: ---> Installing dependencies
  13. remote: Reading package lists...
  14. remote: Building dependency tree...
  15. remote: Reading state information...
  16. remote: python-dev is already the newest version.
  17. remote: libmysqlclient-dev is already the newest version.
  18. remote: 0 upgraded, 0 newly installed, 0 to remove and 15 not upgraded.
  19. remote: Requirement already satisfied (use --upgrade to upgrade): Django==1.4.1 in /usr/local/lib/python2.7/dist-packages (from -r /home/application/current/requirements.txt (line 1))
  20. remote: Requirement already satisfied (use --upgrade to upgrade): MySQL-python==1.2.3 in /usr/local/lib/python2.7/dist-packages (from -r /home/application/current/requirements.txt (line 2))
  21. remote: Requirement already satisfied (use --upgrade to upgrade): South==0.7.6 in /usr/local/lib/python2.7/dist-packages (from -r /home/application/current/requirements.txt (line 3))
  22. remote: Requirement already satisfied (use --upgrade to upgrade): gunicorn==0.14.6 in /usr/local/lib/python2.7/dist-packages (from -r /home/application/current/requirements.txt (line 4))
  23. remote: Cleaning up...
  24. remote:
  25. remote: ---> Restarting your app
  26. remote:
  27. remote: ---> Running restart:after
  28. remote:
  29. remote: ---> Deploy done!
  30. remote:
  31. To git@192.168.50.4.nip.io:blog.git
  32. a780de9..1b675b8 master -> master

顺利完成!现在我们有了一个部署在tsuru上,使用MySQL服务的Django项目,

进一步探索

更多信息,可以查看tsuru文档,或者阅读tsuru命令完全使用指南

原文: http://doc.oschina.net/tsuru-paas?t=52795