LoginSignup
10
10

More than 5 years have passed since last update.

opsworksでDjangoをデプロイ

Last updated at Posted at 2014-12-23

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

はじめに

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

opsworks今北産業

AWS公式

  • 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で定義していればよしなに。

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まわり

ここここを参考に、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まわり

ここらへんを参考にがんばる

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の起動スクリプトまわり

ここを参考に。
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

ここで、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感が無くてすみません…

10
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
10