Google App Engineの開発サーバをApacheのVirtualHostにマッピングする

Google App Engineの開発サーバはdev_appserver.pyを起動してなくちゃいけなくて面倒。
デフォルトでは、ポート8080で127.0.0.1に対してバインドするようになってる。
Tomcatの動いてるサーバを使って外から開発したい場合、portとaddressを指定しなくちゃいけなくて更に面倒。
ポートはアプリ1つ1つ変えないといけなくて面倒。
ブラウザからアクセスする時も、URLはhttp://example.com:8081/とポートも打たなきゃいけなくて面倒。

python dev_appserver.py --port=8081 --address=0.0.0.0 /var/www/html/gae

 
理想は、80番ポートで待ち受けてて、VirtualHostを1つ割り当てる事。
調べたけどそういう事は出来なそうだったから、Apacheのmod_proxyで出来ないかな、と思ってやってみた。
リバースプロキシで、1つのVirtualHostへのアクセスをhttp://127.0.0.1:8081/に転送するようにしたら動いた。
 
でも、常に起動しっぱなしにする場合、dev_appserver.pyはnohupで起動させたりしなくちゃいけない。
それもまた面倒だったから、設定と管理が出来るスクリプトを書いてみた。
とりあえず名前はGAEPorxy。
Pythonの流儀は解らないけど、一応動いてる。
今回初めてPythonやってみたけど、個人的にはあの三項演算子はないと思う。
 

dev_appserver はローカル テスト用に設計されており、外部接続はデフォルトでは許可されません。実行するときに -a フラグを使用して上書きできますが、このようにすると SDK のセキュリティが強化されず、脆弱性が生じることがあるため、推奨しません。

http://code.google.com/intl/ja/appengine/kb/general.html#sdk

 

GAEProxyの使い方

ヘルプ表示
# service gaeproxy help
usage: /etc/init.d/gaeproxy subcommand

  subcommand(config):
    list     show domain list
    add      add domain
             /etc/init.d/gaeproxy add [domain dir]
    del      delete domain
             /etc/init.d/gaeproxy del [id]
    mod      modify domain
             /etc/init.d/gaeproxy mod [id domain dir]
    make     make apache config

  subcommand(run):
    start    start server
    stop     stop server
    fstop    stop server (force)
    restart  restart server

  subcommand(help):
    help     show this message

 

プロキシを立てたいドメインとGAE開発ディレクトリと登録
service gaeproxy add
service gaeproxy add gae.example.com /var/www/html/gae

 

登録内容削除

2つ目の引数はID。
IDはlistで確認。

service gaeproxy del
service gaeproxy del 1

 

登録内容編集

1つ目の引数は、編集するデータのID。
2つ目の引数は、編集後のドメイン
3つ目の引数は、編集後のディレクトリ。

service gaeproxy mod
service gaeproxy mod 1 gaegae.example.com /var/www/html/gaegae

 

登録した内容を確認
service gaeproxy list

 

登録した内容でhttpd.confを作成・Apacheリロード

add、del、modをしたらこれをする。

service gaeproxy make

 

GAE開発サーバ起動

使用ポートは、8100+ID。

service gaeproxy start

 

GAE開発サーバ停止
service gaeproxy stop

 

GAE開発サーバ強制停止

実行してPythonのエラーが出たらまずこれ。

service gaeproxy fstop

 

GAE開発サーバ再起動

中身は、stop startをやってる。

service gaeproxy restart

 

GAE開発サーバの起動状態
service gaeproxy status

 

インストール

gaeproxyのファイル群一式は/usr/local/gaeproxyに保存。
Google App EngineSDKは/usr/local/google_appengineにあると仮定。

chmod +x /usr/local/gaeproxy/gaeproxy.py
ln -s /usr/local/gaeproxy/gaeproxy.py /etc/rc.d/init.d/gaeproxy

# 環境変数でプロキシを使ってる事を悟られないようにするパッチ
patch /usr/local/google_appengine/google/appengine/tools/dev_appserver.py < /usr/local/gaeproxy/dev_appserver.patch

chkconfig --add gaeproxy

 

動作環境

# python -V
Python 2.6.1
# python -V
Python 2.5.1
# sqlite3 -version
3.3.6
# httpd -version
Server version: Apache/2.2.3
Server built:   Nov 12 2009 18:43:47

 

ハマった所

ブート時はホームディレクトリが/になってる

1発目の起動でこんなの聞かれる。
ブート時にinit.d内のスクリプトを叩いてるのはrootだけど、ホームディレクトリは/になってるっぽい。
chkconfigに登録して使う場合は、/.appcfg_nagが必要だった。

Allow dev_appserver to check for updates on startup? (Y/n):

 

CentOSの場合だいたいPython2.4をみてる

Yumのせいで。
PATH上にある「python」という名のpython2.4を全て消して、使いたいバージョンのpythonシンボリックリンクを貼る。
Yumの1行目はpython2.4を直接指定。
これで2.4がデフォルトのpythonにならなくなる。
 

ソース

gaeproxy.py
#!/usr/bin/env python
#
# coding: utf-8
# chkconfig: 2345 99 01
# description: GAEProxy is a Google App Engine development server manager.
# processname: gaeproxy

import os
import sys
import pwd
import subprocess
import re
import time
import sqlite3

class GAEProxy(object):
	appserver = '/usr/local/google_appengine/dev_appserver.py'
	piddir = '/var/run/gaeproxy'
	logdir = '/var/log/gaeproxy'
	apacheconf = '/etc/httpd/conf.d/gaeproxy.conf'
	port = 8100

	def __init__(self):
		dir_path = os.path.dirname(os.path.realpath(__file__))
		db_path = os.path.join(dir_path, 'config.db')
		home_path = os.path.expanduser('~/')
		nag_path = os.path.join(home_path, '.appcfg_nag')
		init_flag = False
		if not os.path.exists(db_path):
			init_flag = True
		self.con = sqlite3.connect(db_path)
		self.con.isolation_level = None
		if init_flag:
			self.db_init()
		if not os.path.exists(self.piddir):
			os.mkdir(self.piddir)
		if not os.path.exists(self.logdir):
			os.mkdir(self.logdir)
		if not os.path.exists(nag_path):
			open(nag_path, 'w').write('opt_in: false\ntimestamp: %f' % time.time())

	def db_init(self):
		sql_drop = """
			DROP TABLE IF EXISTS appdata;
		"""
		sql_create = """
			CREATE TABLE appdata (
				id INTEGER PRIMARY KEY,
				domain VARCHAR(255),
				dir VARCHAR(255)
			);
		"""
		self.con.execute(sql_drop)
		self.con.execute(sql_create)

	def command_list(self):
		sql_len = 'SELECT MAX(LENGTH(id)), MAX(LENGTH(domain)), MAX(LENGTH(dir)) FROM appdata;'
		id_len = 2
		dom_len = 5
		dir_len = 9
		for id, dom, dir in self.con.execute(sql_len):
			id_len = max(id_len, id)
			dom_len = max(dom_len, dom)
			dir_len = max(dir_len, dir)
		sql_list = 'SELECT * FROM appdata;'
		list = self.con.execute(sql_list)
		print (' %-'+str(id_len)+'s | %-'+str(dom_len)+'s | %s') % ('id', 'domain', 'directory')
		print '-' + '-'*id_len + '-+-' + '-'*dom_len + '-+-' + '-'*dir_len + '-'
		for id, dom, dir in list:
			print (' %'+str(id_len)+'u | %-'+str(dom_len)+'s | %s') % (id, dom, dir)

	def command_add(self):
		domain = ''
		dir = ''
		ok = 'Y'
		if len(sys.argv)==4:
			domain = sys.argv[2]
			dir = sys.argv[3]
		else:
			while len(domain)==0:
				domain = raw_input('domain: ').strip()
			while len(dir)==0:
				dir = raw_input('dir: ').strip()
			print 'confirm'
			print ' type  : add'
			print ' domain: %s' % domain
			print ' dir   : %s' % dir
			ok = raw_input('ok? ').strip().upper()
		if ok=='Y' or ok == 'YES':
			insert_sql = 'INSERT INTO appdata (domain, dir) VALUES (?, ?);'
			result = self.con.execute(insert_sql, (domain, dir)).rowcount
			if result:
				print '%s rows added.' % (result)
			else:
				print 'Error!'
		else:
			print 'Cancel.'

	def command_del(self):
		id = 0
		ok = 'Y'
		if len(sys.argv)==3:
			if sys.argv[2].isdigit():
				id = int(sys.argv[2])
		else:
			id2 = ''
			while not id2.isdigit():
				id2 = raw_input('id: ').strip()
			id = int(id2)
			print 'confirm'
			print ' type: delete'
			print ' id  : %u' % id
			ok = raw_input('ok? ').strip().upper()
		if ok=='Y' or ok=='YES':
			delete_sql = 'DELETE FROM appdata WHERE id=:id;'
			result = self.con.execute(delete_sql, {'id':id}).rowcount
			print '%s rows deleted.' % (result)
		else:
			print 'Cancel.'

	def command_mod(self):
		id = 0
		domain = ''
		dir = ''
		ok = 'Y'
		if len(sys.argv)==5:
			if sys.argv[2].isdigit():
				id = int(sys.argv[2])
			domain = sys.argv[3]
			dir = sys.argv[4]
		else:
			id2 = ''
			while not id2.isdigit():
				id2 = raw_input('id: ').strip()
			id = int(id2)
			while len(domain)==0:
				domain = raw_input('domain: ').strip()
			while len(dir)==0:
				dir = raw_input('dir: ').strip()
			print 'confirm'
			print ' type  : modify'
			print ' id    : %u' % id
			print ' domain: %s' % domain
			print ' dir   : %s' % dir
			ok = raw_input('ok? ').strip().upper()
		if ok=='Y' or ok == 'YES':
			update_sql = 'UPDATE appdata SET domain=:domain, dir=:dir WHERE id=:id;'
			result = self.con.execute(update_sql, {'id':id, 'domain':domain, 'dir':dir}).rowcount
			if result:
				print '%s rows modified.' % (result)
			else:
				print 'Error!'
		else:
			print 'Cancel.'

	def command_start(self):
		sql_list = 'SELECT * FROM appdata;'
		list = self.con.execute(sql_list)
		for id, domain, dir in list:
			pidfile = os.path.join(self.piddir, domain) + '.pid'
			logfile = os.path.join(self.logdir, domain) + '.log'
			if os.path.exists(pidfile) and self.checkPID(pidfile):
				print 'Running: %s' % domain
			else:
				cmd = "nohup %s --port=%u '%s' > '%s' 2>&1 &" % (self.appserver, self.port+id, dir, logfile)
				subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE).wait()
				pid = self.getPID(pattern = (' --port=%u ' % (self.port+id)))
				if 0 < pid:
					open(pidfile, 'w').write(str(pid))
					print 'Starting: %s(%u)' % (domain, pid)
				else:
					print 'Error: %s' % domain

	def command_stop(self):
		list = os.listdir(self.piddir)
		for item in list:
			pidfile = os.path.join(self.piddir, item)
			if os.path.exists(pidfile):
				pid = open(pidfile, 'r').read()
				cmd = 'kill %s' % pid
				ret = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE).wait()
				os.remove(pidfile)
				if ret==0:
					print 'Stopping: %s' % item
				else:
					print 'Error: %s' % item

	def command_fstop(self):
		cmd = "ps x | grep '%s' | grep -v grep | awk '{print $1}'" % self.appserver
		list = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE).stdout
		for pid in list.readlines():
			if 0 < len(pid):
				cmd = 'kill %s' % pid
				ret = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE).wait()
				if ret==0:
					print 'Stopping: %s' % pid
				else:
					print 'Error: %s' % pid

	def command_restart(self):
		print '[Stopping]'
		self.command_stop()
		time.sleep(0.2)
		print '[Starting]'
		self.command_start()

	def command_status(self):
		sql_list = 'SELECT * FROM appdata;'
		list = self.con.execute(sql_list)
		for id, domain, dir in list:
			pidfile = os.path.join(self.piddir, domain) + '.pid'
			if os.path.exists(pidfile) and self.checkPID(pidfile):
				print 'Running: %s' % domain
			else:
				print 'Stopped: %s' % domain

	def command_make(self):
		sql_list = 'SELECT * FROM appdata;'
		list = self.con.execute(sql_list)
		conf = ''
		for id, domain, dir in list:
			conf += """
				<VirtualHost *:80>
					DocumentRoot /tmp
					ServerName %s
					<Proxy *>
						Order deny,allow
						Allow from all
					</Proxy>
					ProxyPass / http://127.0.0.1:%u/
					ProxyPassReverse / http://127.0.0.1:%u/
				</VirtualHost>
				""" % (domain, self.port+id, self.port+id)
		conf = re.sub('\t\t\t\t', '', conf)
		print 'Writing %s...' % os.path.basename(self.apacheconf)
		open(self.apacheconf, 'w').write(conf)
		print 'Reload httpd...'
		cmd = 'service httpd reload'
		subprocess.Popen(cmd, shell =True, stdout = subprocess.PIPE, stderr = subprocess.PIPE).wait()
		print 'Finish'

	def command_help(self):
		help = """
			usage: %s subcommand

			  subcommand(config):
			    list     show domain list
			    add      add domain
			             %s add [domain dir]
			    del      delete domain
			             %s del [id]
			    mod      modify domain
			             %s mod [id domain dir]
			    make     make apache config

			  subcommand(run):
			    start    start server
			    stop     stop server
			    fstop    stop server (force)
			    restart  restart server

			  subcommand(help):
			    help     show this message
		""" % (sys.argv[0], sys.argv[0], sys.argv[0], sys.argv[0])
		help = re.sub('\t\t\t', '', help).strip()
		print help

	def getPID(self, pattern):
		cmd = "ps x | sed 's/^ *//' | grep '%s' | grep '%s' | grep -v grep | awk '{print $1}'" % (self.appserver, pattern)
		result = subprocess.Popen(cmd, shell = True, stdout = subprocess.PIPE, stderr = subprocess.PIPE).stdout.readline()
		if 0 < len(result):
			return int(result)
		return 0

	def checkPID(self, pidfile):
		pid = open(pidfile, 'r').read()
		return 0 < self.getPID(pattern = ('^%s ' % pid))

if __name__ == '__main__':
	if pwd.getpwuid(os.getuid())[0]=='root':
		gae = GAEProxy()
		sub = ['list', 'add', 'del', 'mod', 'start', 'stop', 'fstop', 'restart', 'status', 'make', 'help']
		if 1 < len(sys.argv) and sys.argv[1].lower() in sub:
			exec 'gae.command_%s()' % sys.argv[1].lower()
		else:
			gae.command_help()
	else:
		print 'Permission denied'

 

dev_appserver.patch
--- google/appengine/tools/dev_appserver.py.backup      2010-01-16 03:52:53.000000000 +0900
+++ google/appengine/tools/dev_appserver.py     2010-01-16 03:57:35.000000000 +0900
@@ -697,6 +697,18 @@
     infile.seek(0)
     env['CONTENT_LENGTH'] = str(len(new_data))
 
+  if ('HTTP_X_FORWARDED_HOST' in env):
+    env['HTTP_HOST'] = env['HTTP_X_FORWARDED_HOST']
+    env.pop('HTTP_X_FORWARDED_HOST')
+
+  if ('HTTP_X_FORWARDED_FOR' in env):
+    env['REMOTE_ADDR'] = env['HTTP_X_FORWARDED_FOR']
+    env.pop('HTTP_X_FORWARDED_FOR')
+
+  if ('HTTP_X_FORWARDED_SERVER' in env):
+    env['SERVER_NAME'] = env['HTTP_X_FORWARDED_SERVER']
+    env.pop('HTTP_X_FORWARDED_SERVER')
+
   return env