opsworksでDjangoをデプロイ


この記事はAWS Advent Calendar 2014の23日目の記事です。

# はじめに

みなさんopsworks使っていますか？
サポートされているアプリ（railsだとかnode.jsだとか）のデプロイは割と簡単ですが、
サポートされていないアプリ（今回はdjango）をどうやってソレっぽくデプロイするか、の奮闘記を。

## opsworks今北産業

[AWS公式](http://aws.amazon.com/jp/opsworks/)

* chef-zero
* pull型プロビジョニング/デプロイ
* 画面ポチポチ

## 下調べ

（※community cookbook使わない方針です）

なるべくopsworksの元々のCookbookに助けてもらう（&壊さない）形で、
Custom Cookbookを作ってDjangoをデプロイしたい
基本的にrails/unicornのデプロイレシピを丸パk...

真似するのは下記のレシピです（attributes抜いちゃいました）

* [deploy](https://github.com/aws/opsworks-cookbooks/tree/release-chef-11.10/deploy)
	* definitions/opsworks_deploy.rb (リソース展開とかrestart)
	* definitions/opsworks_rails.rb (database.ymlとかの配置)
	* recipes/rails.rb (deploy/opsworks_railsをラップしてる)
* [rails](https://github.com/aws/opsworks-cookbooks/tree/release-chef-11.10/rails)
	* libraries/rails_configuration.rb (bundle installの実行)
	* recipes/configure.rb (database.ymlとかの配置)
	* templates/default/database.yml.erb (database.ymlのテンプレ)
* [unicorn](https://github.com/aws/opsworks-cookbooks/tree/release-chef-11.10/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がどうマージされるのかについては、[ここ](http://docs.aws.amazon.com/ja_jp/opsworks/latest/userguide/workingcookbook-chef11-10.html)や[ここ](http://docs.aws.amazon.com/ja_jp/opsworks/latest/userguide/workingcookbook-installingcustom-enable.html)を参考に


# いざレシピ書かんとす

結構長くなるので要所だけ抜粋で。


## rails -> django

### pip installまわり

[ここ](https://github.com/aws/opsworks-cookbooks/blob/release-chef-11.10/rails/libraries/rails_configuration.rb)を参考に、virtualenvを作ってpip install -r requirements.txtするように（bundle install的な）
今回は、/home/deploy/.venvs/<your_app_name>配下にvirtualenvを作るようにしました。

pythonのパスは他のcookbookで定義していればよしなに。

```django/libraries/django_configration.rb
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まわり

[ここ](https://github.com/aws/opsworks-cookbooks/blob/release-chef-11.10/rails/recipes/configure.rb)や[ここ](https://docs.djangoproject.com/en/1.7/topics/db/multi-db/)を参考に、database.pyとかを生成するレシピを作る
（アプリ側のsettings.pyなり、settings_production.pyでimportしてもらうという前提）


```django/recipes/configure.rb
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
```

```django/templates/default/database.py.erb
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まわり

[ここらへん](http://docs.gunicorn.org/en/develop/configure.html)を参考にがんばる

```gunicorn/template/default/gunicorn.conf.py.erb
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の起動スクリプトまわり

[ここ](https://github.com/aws/opsworks-cookbooks/blob/release-chef-11.10/unicorn/templates/default/unicorn.service.erb)を参考に。
rails_envに合わせて、django_envみたいなattribute作っておいて、それによってsettings_{production|staging|development}.pyとか切り替える感じ

pythonの起動スクリプトをrubyで書くとか謎な感じ…なのでpythonで書いてもアリかと

```templates/default/gunicorn.service.erb
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はよしなに）


```custom_deploy/definitions/opsworks_django.rb
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

[ここ](https://github.com/aws/opsworks-cookbooks/blob/release-chef-11.10/deploy/definitions/opsworks_deploy.rb#L63)で、application_type が otherだと リソース展開してくれないので...（オーバーライドしてもOKですが）

```custom_deploy/definitions/custom_deploy.rb
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
```

最後！ラップしてる部分！

```custom_deploy/recipes/django.rb
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を書くます

```custom.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感が無くてすみません…