この記事はAWS Advent Calendar 2014の23日目の記事です。
はじめに
みなさんopsworks使っていますか?
サポートされているアプリ(railsだとかnode.jsだとか)のデプロイは割と簡単ですが、
サポートされていないアプリ(今回はdjango)をどうやってソレっぽくデプロイするか、の奮闘記を。
opsworks今北産業
- chef-zero
- pull型プロビジョニング/デプロイ
- 画面ポチポチ
下調べ
(※community cookbook使わない方針です)
なるべくopsworksの元々のCookbookに助けてもらう(&壊さない)形で、
Custom Cookbookを作ってDjangoをデプロイしたい
基本的にrails/unicornのデプロイレシピを丸パk...
真似するのは下記のレシピです(attributes抜いちゃいました)
-
deploy
- definitions/opsworks_deploy.rb (リソース展開とかrestart)
- definitions/opsworks_rails.rb (database.ymlとかの配置)
- recipes/rails.rb (deploy/opsworks_railsをラップしてる)
-
rails
- libraries/rails_configuration.rb (bundle installの実行)
- recipes/configure.rb (database.ymlとかの配置)
- templates/default/database.yml.erb (database.ymlのテンプレ)
-
unicorn
- definitions/unicorn_web_app.rb (nginxのconfigファイルの配置)
- recipes/default.rb (unicornのinstall)
- recipes/rails.rb (unicornの設定ファイル・起動スクリプトを作る)
- recipes/stop.rb (unicornの停止)
- templates/default/nginx_uniconn_webapp.erb (nginxのconfigファイルのテンプレ)
- templates/default/unicorn.conf.erb (unicornの設定ファイルテンプレ)
- templates/default/unicorn.service.erb (unicornの起動スクリプトテンプレ)
それぞれテキトウに、以下のように置き換えてCustom Cookbookを作ります
- deploy -> custom_deploy
- rails -> django
- unicorn -> gunicorn
※ Custom Cookbookがどうマージされるのかについては、ここやここを参考に
いざレシピ書かんとす
結構長くなるので要所だけ抜粋で。
rails -> django
pip installまわり
ここを参考に、virtualenvを作ってpip install -r requirements.txtするように(bundle install的な)
今回は、/home/deploy/.venvs/配下にvirtualenvを作るようにしました。
pythonのパスは他のcookbookで定義していればよしなに。
module OpsWorks
module DjangoConfiguration
def self.pip(app_name, app_config, app_root_path)
if File.exists?("#{app_root_path}/requirements.txt")
Chef::Log.info("requirements.txt detected.")
Chef::Log.info(OpsWorks::ShellOut.shellout("sudo su - #{app_config[:user]} -c 'mkdir -p #{app_config[:home]}/.venvs'"))
unless File.directory?("#{app_config[:home]}/.venvs/#{app_name}")
Chef::Log.info(OpsWorks::ShellOut.shellout("sudo su - #{app_config[:user]} -c 'virtualenv-2.7 #{app_config[:home]}/.venvs/#{app_name} --python=/usr/bin/python2.7'"))
end
Chef::Log.info("Running pip install.")
Chef::Log.info("#{app_config[:home]}/.venvs/#{app_name}/bin/pip2.7 install -r #{app_root_path}/requirements.txt")
Chef::Log.info(OpsWorks::ShellOut.shellout("sudo su - #{app_config[:user]} -c '#{app_config[:home]}/.venvs/#{app_name}/bin/pip2.7 install -r #{app_root_path}/requirements.txt 2>&1'"))
end
end
end
end
djangoのconfigまわり
ここやここを参考に、database.pyとかを生成するレシピを作る
(アプリ側のsettings.pyなり、settings_production.pyでimportしてもらうという前提)
include_recipe "deploy"
node[:deploy].each do |application, deploy|
deploy = node[:deploy][application]
if deploy[:application_type] != 'other' && deploy[:custom_application_type] == 'django'
Chef::Log.debug("Skipping goserver::configure application #{application} as it is not a django app")
next
end
execute "restart Django app #{application}" do
cwd deploy[:current_path]
command node[:opsworks][:django_stack][:restart_command]
action :nothing
end
template "#{deploy[:deploy_to]}/shared/config/database.py" do
source 'database.py.erb'
cookbook 'django'
mode '0660'
group deploy[:group]
owner deploy[:user]
variables(:database => deploy[:database])
notifies :run, "execute[restart Django app #{application}]"
only_if do
deploy[:database][:host].present? && File.directory?("#{deploy[:deploy_to]}/shared/config/")
end
end
# 中略
end
DATABASES = {
'default': {
'ENGINE': <%= (@database[:engine] || 'django.db.backends.mysql').to_s.inspect %>,
'HOST': <%= (@database[:host] || 'localhost').to_s.inspect %>,
'PORT': <%= (@database[:port] || 3306).to_i %>,
'NAME': <%= @database[:database].to_s.inspect %>,
'USER': <%= @database[:username].to_s.inspect %>,
'PASSWORD': <%= @database[:password].to_s.inspect %>,
},
<% if @database[:additional_databases] -%>
<!-- ... 中略 ... -->
<% end -%>
}
unicorn -> gunicorn
基本的には s/unicorn/gunicorn/しまくる感じで
gunicornのconfigまわり
ここらへんを参考にがんばる
workers = <%= node[:gunicorn][:worker_processes].to_i %>
worker_class = <%= node[:gunicorn][:worker_class].to_s.inspect %>
worker_connections = <%= node[:gunicorn][:worker_connections].to_i %>
timeout = <%= node[:gunicorn][:worker_timeout].to_i %>
keepalive = <%= node[:gunicorn][:worker_keepalive].to_i %>
max_requests = <%= node[:gunicorn][:worker_max_requests].to_i %>
# 中略
gunicornの起動スクリプトまわり
ここを参考に。
rails_envに合わせて、django_envみたいなattribute作っておいて、それによってsettings_{production|staging|development}.pyとか切り替える感じ
pythonの起動スクリプトをrubyで書くとか謎な感じ…なのでpythonで書いてもアリかと
APP_NAME = <%= @application.to_s.inspect %>
ROOT_PATH = <%= @deploy[:deploy_to].to_s.inspect %>
DJANGO_APP = "<%= (@deploy[:django_project] || @application).to_s %>.wsgi"
DJANGO_ENV = "DJANGO_SETTINGS_MODULE=<%= (@deploy[:django_project] || @application).to_s %>.settings_<%= @deploy[:django_env].to_s %>"
# 中略
def start_gunicorn
run_and_ignore_exitcode_and_print_command "cd #{ROOT_PATH}/current && <%= @deploy[:home] %>/.venvs/#{APP_NAME}/bin/gunicorn #{DJANGO_APP} --env #{DJANGO_ENV} --daemon -c #{ROOT_PATH}/shared/config/gunicorn.conf.py"
end
# 中略
deploy -> custom_deploy
djangoのconfigまわり
こっちのレシピでは、deploy内でrestartするので、notifiesは不要
(今回おもいっきりスルーしてますが、pythonのinstallはよしなに)
define :opsworks_django do
application = params[:app]
deploy = params[:deploy_data]
include_recipe node[:opsworks][:django_stack][:python_recipe] # python27::install
include_recipe node[:opsworks][:django_stack][:recipe] # gunicorn::django
template "#{deploy[:deploy_to]}/shared/config/database.py" do
source 'database.py.erb'
cookbook 'django'
mode '0660'
group deploy[:group]
owner deploy[:user]
variables(:database => deploy[:database])
only_if do
deploy[:database][:host].present? && File.directory?("#{deploy[:deploy_to]}/shared/config/")
end
end
# 中略
end
deploy&restart
ここで、application_type が otherだと リソース展開してくれないので...(オーバーライドしてもOKですが)
define :custom_deploy do
application = params[:app]
deploy = params[:deploy_data]
# 中略
# setup deployment & checkout
if deploy[:scm]
# 中略
# django application restart
if deploy[:application_type] == 'other' && deploy[:custom_application_type] == 'django'
restart_command "sleep #{deploy[:sleep_before_restart]} && #{node[:opsworks][:django_stack][:restart_command]}"
end
# 中略
before_migrate do
link_tempfiles_to_current_release
if deploy[:application_type] == 'other' && deploy[:custom_application_type] == 'python-scripts'
# install pip packages for python applications
OpsWorks::DjangoConfiguration.pip(application, node[:deploy][application], release_path)
end
# run user provided callback file
run_callback_from_file("#{release_path}/deploy/before_migrate.rb")
end
end
end
ruby_block "change HOME back to /root after source checkout" do
block do
ENV['HOME'] = "/root"
end
end
# for django stack
if deploy[:application_type] == 'other' && deploy[:custom_application_type] == 'django'
case node[:opsworks][:django_stack][:name]
when 'nginx_gunicorn'
gunicorn_web_app do
application application
deploy deploy
end
else
raise "Unsupport Django stack"
end
end
# 中略
end
最後!ラップしてる部分!
include_recipe 'deploy'
node[:deploy].each do |application, deploy|
if deploy[:application_type] != 'other' || deploy[:custom_application_type] != 'django'
Chef::Log.debug("Skipping custom_deploy::django application #{application} as it is not a django app")
next
end
opsworks_deploy_dir do
user deploy[:user]
group deploy[:group]
path deploy[:deploy_to]
end
custom_django do
deploy_data deploy
app application
end
custom_deploy do # オーバーライドする場合は opsworks_deployのままで
deploy_data deploy
app application
end
end
いざデプロイせんとす
Stack Settings
こんなかんじでjsonを書くます
"opsworks": {"django_stack": {"name": "nginx_gunicorn"} },
"deploy": {
"<your_app_name>": {
"user": "deploy", "group": "nginx",
"custom_application_type": "django",
"symlink_before_migrate": {
"config/database.py": "<your_app_name>/database.py"
},
"database": {
...
}
}
}
Layers Settings
Opsworksにて、Layers->Custom Chef Recipesで、下記を指定すればOK
(どこかでinclude_recipe python27::installしてある前提)
- Setup: (特になし)
- Configure:
django::configure
- Deploy:
custom_deploy::django
- Undeploy: (作っていればcustom_deploy::django-undeploy)
- Shutfown: (特になし)
Let's Deploy
という感じでデプロイできます
まとめ
という感じで、Webサーバ/プロセスマネージャ/フレームワーク/言語のセットアップをすれば、
たいていのアプリは似たような感じでデプロイできると思います。
DjangoデプロイのCookbookはそのうち公開できたらしたいです。
からの所感
サポートされていないアプリ(=フレームワーク/言語)載せるのは結構辛いけど、
使いまわせるレシピが一度書ければ、後はMgmtConsoleポチポチで、
「明日新しいサービスリリースしたいんだけど」→「じゃあ午前中に環境作るねー☆(ゝω・)vキャピ」
ぐらいのノリで対応できるのでだいぶ省エネできます
opsworksに関して、個人的にもっと熱弁したいことはたくさんあるのですが、
- バッチサーバのデプロイをどうするか
- サーバログイン権限の管理まわりがクッソ幸せ
- security groupとinstance profileがクッソ幸せ
- インスタンス自身の設定はインスタンス自身にさせよう(例えば起動時にroute53登録とか)
的な話を別途書ければ。。。
// opsworksでdjango公式にサポートして欲しいなぁ…
// AWS Advent CalendarなのにあんまりAWS感が無くてすみません…