├── lib ├── django_capistrano.rb └── capistrano │ └── django.rb ├── .gitignore ├── test ├── Gemfile ├── Capfile ├── config │ ├── deploy │ │ └── vagrant.rb │ └── deploy.rb ├── spec │ ├── spec_helper.rb │ └── web │ │ └── django_spec.rb ├── Rakefile ├── Vagrantfile └── Gemfile.lock ├── capistrano-django.gemspec └── README.md /lib/django_capistrano.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .DS_Store 3 | *.gem 4 | .vagrant 5 | .spec 6 | .rspec 7 | -------------------------------------------------------------------------------- /test/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'serverspec' 4 | gem 'fog' 5 | -------------------------------------------------------------------------------- /test/Capfile: -------------------------------------------------------------------------------- 1 | require 'capistrano/setup' 2 | require 'capistrano/deploy' 3 | require_relative '../lib/capistrano/django' 4 | -------------------------------------------------------------------------------- /test/config/deploy/vagrant.rb: -------------------------------------------------------------------------------- 1 | set :stage, :vagrant 2 | server 'localhost', user: 'vagrant', roles: %w{web jobs}, password: 'vagrant', port: 2222 3 | -------------------------------------------------------------------------------- /test/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'serverspec' 2 | require 'net/ssh' 3 | require 'tempfile' 4 | 5 | set :backend, :ssh 6 | 7 | host = ENV['TARGET_HOST'] 8 | 9 | `vagrant up #{host}` 10 | 11 | config = Tempfile.new('', Dir.tmpdir) 12 | config.write(`vagrant ssh-config #{host}`) 13 | config.close 14 | 15 | options = Net::SSH::Config.for(host, [config.path]) 16 | 17 | options[:user] ||= Etc.getlogin 18 | 19 | set :host, options[:host_name] || host 20 | set :ssh_options, options 21 | -------------------------------------------------------------------------------- /test/config/deploy.rb: -------------------------------------------------------------------------------- 1 | set :application, 'test_deploy' 2 | set :scm, :git 3 | set :repo_url, 'https://github.com/mattjmorrison/test_deploy.git' 4 | set :pip_requirements, 'requirements/base.txt' 5 | set :django_settings, 'production' 6 | set :django_settings_dir, 'test_deploy/settings' 7 | set :wsgi_path, 'test_deploy' 8 | set :wsgi_file_name, 'wsgi.py' 9 | set :create_s3_bucket, false 10 | 11 | set :aws_access_key, 'asdf' 12 | set :aws_secret_key, 'wxyz' 13 | set :s3_bucket_prefix, 'my-bucket-prefix' 14 | -------------------------------------------------------------------------------- /capistrano-django.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | 3 | s.name = "capistrano-django" 4 | s.version = "4.0.3" 5 | 6 | s.homepage = "http://github.com/mattjmorrison/capistrano-django" 7 | s.summary = %q{capistrano-django - Welcome to easy deployment with Ruby over SSH for Django} 8 | s.description = %q{capistrano-django provides a solid basis for common django deployment} 9 | 10 | s.files = Dir["lib/**/*.rb"] 11 | s.add_dependency "capistrano", "~> 3" 12 | 13 | s.author = "Matthew J. Morrison" 14 | s.email = "mattjmorrison@mattjmorrison.com" 15 | 16 | end 17 | -------------------------------------------------------------------------------- /test/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rspec/core/rake_task' 3 | 4 | task :spec => 'spec:all' 5 | task :default => :spec 6 | 7 | namespace :spec do 8 | targets = [] 9 | Dir.glob('./spec/*').each do |dir| 10 | next unless File.directory?(dir) 11 | target = File.basename(dir) 12 | target = "_#{target}" if target == "default" 13 | targets << target 14 | end 15 | 16 | task :all => targets 17 | task :default => :all 18 | 19 | targets.each do |target| 20 | original_target = target == "_default" ? target[1..-1] : target 21 | desc "Run serverspec tests to #{original_target}" 22 | RSpec::Core::RakeTask.new(target.to_sym) do |t| 23 | ENV['TARGET_HOST'] = original_target 24 | t.pattern = "spec/#{original_target}/*_spec.rb" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/spec/web/django_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | path = '/var/www/current' 4 | manage_py = "#{path}/virtualenv/bin/python #{path}/manage.py" 5 | 6 | describe file("#{path}/virtualenv") do 7 | it { should be_directory } 8 | end 9 | 10 | describe file("#{path}/test_deploy/settings/deployed.py") do 11 | it { should be_file } 12 | end 13 | 14 | describe file("#{path}/test_deploy/live.wsgi") do 15 | it { should be_file } 16 | end 17 | 18 | describe file('/home/vagrant/static') do 19 | it { should be_directory } 20 | end 21 | 22 | describe command("#{manage_py} production migrate --list") do 23 | its(:stdout) { should_not contain '\[ \]' } 24 | end 25 | 26 | # describe command("#{manage_py} production display_s3_settings") do 27 | # its(:stdout) { should match %r|^STATIC_URL = https://s3.amazonaws.com/my-bucket-prefix-\d{14}/$| } 28 | # its(:stdout) { should match /^AWS_ACCESS_KEY_ID = asdf$/ } 29 | # its(:stdout) { should match /^AWS_SECRET_ACCESS_KEY = wxyz$/ } 30 | # its(:stdout) { should match /^AWS_STORAGE_BUCKET_NAME = my-bucket-prefix-\d{14}$/ } 31 | # end 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Capistrano Django 2 | 3 | **A set of tasks built ontop of Capistrano to assist with Django deployments** 4 | 5 | example config file: 6 | 7 | ``` ruby 8 | set :application, 'app_name' 9 | set :scm, :git 10 | set :repo_url, 'git@github.com:username/repo_name.git' 11 | set :django_settings_dir, 'app_name/settings' 12 | set :pip_requirements, 'requirements/base.txt' 13 | set :keep_releases, 5 14 | set :nginx, true 15 | set :deploy_to, '/www/app_name.com' 16 | set :wsgi_file, 'app_name.wsgi' 17 | set :npm_tasks, {:grunt => 'do_something', :gulp => 'something_else'} 18 | set :stage, :production 19 | set :django_settings, 'production' 20 | role :web, "user@127.0.0.1" 21 | ``` 22 | 23 | Ordinarily, capistrano-django builds a separate virtualenv per-deploy. 24 | 25 | If you include: 26 | ``` ruby 27 | set :shared_virtualenv, true 28 | ``` 29 | in your configuration file, it will instead create a virtualenv in the `shared_path`, and 30 | symlink it into the release path. It will build it via requirements only when they differ 31 | from those of the last release. 32 | 33 | **Author:** Matthew J. Morrison. [Follow me on Twitter](https://twitter.com/mattjmorrison) 34 | -------------------------------------------------------------------------------- /test/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | VAGRANTFILE_API_VERSION = "2" 5 | 6 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 7 | # TODO put box url in here 8 | config.vm.box = "ubuntu-14.04" 9 | 10 | config.vm.define :web do |web| 11 | web.vm.network "forwarded_port", guest: 80, host: 8080 12 | web.vm.provision "shell", inline: <<-SCRIPT 13 | sudo apt-get update -y 14 | sudo apt-get upgrade -y 15 | sudo apt-get install -y git build-essential python python-virtualenv apache2 libapache2-mod-wsgi 16 | sudo mkdir -p /var/www/shared /var/www/releases 17 | mkdir -p /home/vagrant/static 18 | sudo chown -R vagrant:www-data /home/vagrant 19 | sudo chown -R vagrant:www-data /var/www 20 | sudo echo 'WSGIPythonHome /var/www/current/virtualenv' > /etc/apache2/sites-available/test-deploy.conf 21 | sudo echo 'WSGIApplicationGroup %{GLOBAL}' >> /etc/apache2/sites-available/test-deploy.conf 22 | sudo echo '' >> /etc/apache2/sites-available/test-deploy.conf 23 | sudo echo ' WSGIScriptAlias / /var/www/current/test_deploy/live.wsgi' >> /etc/apache2/sites-available/test-deploy.conf 24 | sudo echo ' WSGIProcessGroup test' >> /etc/apache2/sites-available/test-deploy.conf 25 | sudo echo ' WSGIDaemonProcess test processes=3 threads=3 display-name=test python-path=/var/www/current/virtualenv/lib/python2.7/site-packages:/var/www/current' >> /etc/apache2/sites-available/test-deploy.conf 26 | sudo echo '' >> /etc/apache2/sites-available/test-deploy.conf 27 | sudo a2dissite 000-default 28 | sudo a2ensite test-deploy.conf 29 | sudo apache2ctl restart 30 | SCRIPT 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /test/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (2.3.1) 5 | builder (3.2.2) 6 | diff-lcs (1.2.5) 7 | excon (0.45.3) 8 | fission (0.5.0) 9 | CFPropertyList (~> 2.2) 10 | fog (1.30.0) 11 | fog-atmos 12 | fog-aws (~> 0.0) 13 | fog-brightbox (~> 0.4) 14 | fog-core (~> 1.27, >= 1.27.4) 15 | fog-ecloud 16 | fog-google (>= 0.0.2) 17 | fog-json 18 | fog-local 19 | fog-powerdns (>= 0.1.1) 20 | fog-profitbricks 21 | fog-radosgw (>= 0.0.2) 22 | fog-riakcs 23 | fog-sakuracloud (>= 0.0.4) 24 | fog-serverlove 25 | fog-softlayer 26 | fog-storm_on_demand 27 | fog-terremark 28 | fog-vmfusion 29 | fog-voxel 30 | fog-xml (~> 0.1.1) 31 | ipaddress (~> 0.5) 32 | nokogiri (~> 1.5, >= 1.5.11) 33 | fog-atmos (0.1.0) 34 | fog-core 35 | fog-xml 36 | fog-aws (0.4.0) 37 | fog-core (~> 1.27) 38 | fog-json (~> 1.0) 39 | fog-xml (~> 0.1) 40 | ipaddress (~> 0.8) 41 | fog-brightbox (0.7.1) 42 | fog-core (~> 1.22) 43 | fog-json 44 | inflecto (~> 0.0.2) 45 | fog-core (1.30.0) 46 | builder 47 | excon (~> 0.45) 48 | formatador (~> 0.2) 49 | mime-types 50 | net-scp (~> 1.1) 51 | net-ssh (>= 2.1.3) 52 | fog-ecloud (0.1.1) 53 | fog-core 54 | fog-xml 55 | fog-google (0.0.5) 56 | fog-core 57 | fog-json 58 | fog-xml 59 | fog-json (1.0.1) 60 | fog-core (~> 1.0) 61 | multi_json (~> 1.0) 62 | fog-local (0.2.1) 63 | fog-core (~> 1.27) 64 | fog-powerdns (0.1.1) 65 | fog-core (~> 1.27) 66 | fog-json (~> 1.0) 67 | fog-xml (~> 0.1) 68 | fog-profitbricks (0.0.2) 69 | fog-core 70 | fog-xml 71 | nokogiri 72 | fog-radosgw (0.0.4) 73 | fog-core (>= 1.21.0) 74 | fog-json 75 | fog-xml (>= 0.0.1) 76 | fog-riakcs (0.1.0) 77 | fog-core 78 | fog-json 79 | fog-xml 80 | fog-sakuracloud (1.0.1) 81 | fog-core 82 | fog-json 83 | fog-serverlove (0.1.2) 84 | fog-core 85 | fog-json 86 | fog-softlayer (0.4.6) 87 | fog-core 88 | fog-json 89 | fog-storm_on_demand (0.1.1) 90 | fog-core 91 | fog-json 92 | fog-terremark (0.1.0) 93 | fog-core 94 | fog-xml 95 | fog-vmfusion (0.1.0) 96 | fission 97 | fog-core 98 | fog-voxel (0.1.0) 99 | fog-core 100 | fog-xml 101 | fog-xml (0.1.2) 102 | fog-core 103 | nokogiri (~> 1.5, >= 1.5.11) 104 | formatador (0.2.5) 105 | inflecto (0.0.2) 106 | ipaddress (0.8.0) 107 | mime-types (2.6.1) 108 | mini_portile (0.6.2) 109 | multi_json (1.11.0) 110 | net-scp (1.2.1) 111 | net-ssh (>= 2.6.5) 112 | net-ssh (2.9.2) 113 | nokogiri (1.6.6.2) 114 | mini_portile (~> 0.6.0) 115 | rspec (3.2.0) 116 | rspec-core (~> 3.2.0) 117 | rspec-expectations (~> 3.2.0) 118 | rspec-mocks (~> 3.2.0) 119 | rspec-core (3.2.3) 120 | rspec-support (~> 3.2.0) 121 | rspec-expectations (3.2.1) 122 | diff-lcs (>= 1.2.0, < 2.0) 123 | rspec-support (~> 3.2.0) 124 | rspec-its (1.2.0) 125 | rspec-core (>= 3.0.0) 126 | rspec-expectations (>= 3.0.0) 127 | rspec-mocks (3.2.1) 128 | diff-lcs (>= 1.2.0, < 2.0) 129 | rspec-support (~> 3.2.0) 130 | rspec-support (3.2.2) 131 | serverspec (2.17.0) 132 | multi_json 133 | rspec (~> 3.0) 134 | rspec-its 135 | specinfra (~> 2.32) 136 | specinfra (2.34.4) 137 | net-scp 138 | net-ssh 139 | 140 | PLATFORMS 141 | ruby 142 | 143 | DEPENDENCIES 144 | fog 145 | serverspec 146 | -------------------------------------------------------------------------------- /lib/capistrano/django.rb: -------------------------------------------------------------------------------- 1 | after 'deploy:updating', 'python:create_virtualenv' 2 | 3 | namespace :deploy do 4 | 5 | desc 'Restart application' 6 | task :restart do 7 | if fetch(:nginx) 8 | invoke 'deploy:nginx_restart' 9 | else 10 | on roles(:web) do |h| 11 | execute "sudo apache2ctl graceful" 12 | end 13 | end 14 | end 15 | 16 | task :nginx_restart do 17 | on roles(:web) do |h| 18 | within release_path do 19 | pid_file = "#{releases_path}/gunicorn.pid" 20 | if test "[ -e #{pid_file} ]" 21 | execute "kill `cat #{pid_file}`" 22 | end 23 | execute "virtualenv/bin/gunicorn", "#{fetch(:wsgi_file)}:application", '-c=gunicorn_config.py', "--pid=#{pid_file}" 24 | end 25 | end 26 | end 27 | 28 | end 29 | 30 | namespace :python do 31 | 32 | def virtualenv_path 33 | File.join( 34 | fetch(:shared_virtualenv) ? shared_path : release_path, "virtualenv" 35 | ) 36 | end 37 | 38 | desc "Create a python virtualenv" 39 | task :create_virtualenv do 40 | on roles(:all) do |h| 41 | execute "virtualenv #{virtualenv_path}" 42 | execute "#{virtualenv_path}/bin/pip install pip<19.2" 43 | execute "#{virtualenv_path}/bin/pip install -r #{release_path}/#{fetch(:pip_requirements)}" 44 | if fetch(:shared_virtualenv) 45 | execute :ln, "-s", virtualenv_path, File.join(release_path, 'virtualenv') 46 | end 47 | end 48 | end 49 | 50 | desc "Set things up after the virtualenv is ready" 51 | task :post_virtualenv do 52 | if fetch(:npm_tasks) 53 | invoke 'nodejs:npm' 54 | end 55 | if fetch(:flask) 56 | invoke 'flask:setup' 57 | else 58 | invoke 'django:setup' 59 | end 60 | end 61 | 62 | end 63 | 64 | after 'python:create_virtualenv', 'python:post_virtualenv' 65 | 66 | namespace :flask do 67 | 68 | task :setup do 69 | on roles(:web) do |h| 70 | execute "ln -s #{release_path}/settings/#{fetch(:settings_file)}.py #{release_path}/settings/deployed.py" 71 | execute "ln -sf #{release_path}/wsgi/wsgi.py #{release_path}/wsgi/live.wsgi" 72 | end 73 | end 74 | 75 | end 76 | 77 | namespace :django do 78 | 79 | def django(args, flags="", run_on=:all) 80 | on roles(run_on) do |h| 81 | manage_path = File.join(release_path, fetch(:django_project_dir) || '', 'manage.py') 82 | execute "#{release_path}/virtualenv/bin/python #{manage_path} #{fetch(:django_settings)} #{args} #{flags}" 83 | end 84 | end 85 | 86 | after 'deploy:restart', 'django:restart_celery' 87 | 88 | desc "Setup Django environment" 89 | task :setup do 90 | if fetch(:django_compressor) 91 | invoke 'django:compress' 92 | end 93 | invoke 'django:compilemessages' 94 | invoke 'django:collectstatic' 95 | invoke 'django:symlink_settings' 96 | if !fetch(:nginx) 97 | invoke 'django:symlink_wsgi' 98 | end 99 | invoke 'django:migrate' 100 | end 101 | 102 | desc "Compile Messages" 103 | task :compilemessages do 104 | if fetch :compilemessages 105 | django("compilemessages") 106 | end 107 | end 108 | 109 | desc "Restart Celery" 110 | task :restart_celery do 111 | if fetch(:celery_name) 112 | invoke 'django:restart_celeryd' 113 | invoke 'django:restart_celerybeat' 114 | end 115 | if fetch(:celery_names) 116 | invoke 'django:restart_named_celery_processes' 117 | end 118 | end 119 | 120 | desc "Restart Celeryd" 121 | task :restart_celeryd do 122 | on roles(:jobs) do 123 | execute "sudo service celeryd-#{fetch(:celery_name)} restart" 124 | end 125 | end 126 | 127 | desc "Restart Celerybeat" 128 | task :restart_celerybeat do 129 | on roles(:jobs) do 130 | execute "sudo service celerybeat-#{fetch(:celery_name)} restart" 131 | end 132 | end 133 | 134 | desc "Restart named celery processes" 135 | task :restart_named_celery_processes do 136 | on roles(:jobs) do 137 | fetch(:celery_names).each { | celery_name, celery_beat | 138 | execute "sudo service celeryd-#{celery_name} restart" 139 | if celery_beat 140 | execute "sudo service celerybeat-#{celery_name} restart" 141 | end 142 | } 143 | end 144 | end 145 | 146 | desc "Run django-compressor" 147 | task :compress do 148 | django("compress") 149 | end 150 | 151 | desc "Run django's collectstatic" 152 | task :collectstatic do 153 | if fetch(:create_s3_bucket) 154 | invoke 's3:create_bucket' 155 | on roles(:web) do 156 | django("collectstatic", "-i *.coffee -i *.less -i node_modules/* -i bower_components/* --noinput --clear") 157 | end 158 | else 159 | django("collectstatic", "-i *.coffee -i *.less -i node_modules/* -i bower_components/* --noinput") 160 | end 161 | 162 | end 163 | 164 | desc "Symlink django settings to deployed.py" 165 | task :symlink_settings do 166 | settings_path = File.join(release_path, fetch(:django_settings_dir)) 167 | on roles(:all) do 168 | execute "ln -s #{settings_path}/#{fetch(:django_settings)}.py #{settings_path}/deployed.py" 169 | end 170 | end 171 | 172 | desc "Symlink wsgi script to live.wsgi" 173 | task :symlink_wsgi do 174 | on roles(:web) do 175 | wsgi_path = File.join(release_path, fetch(:wsgi_path, 'wsgi')) 176 | wsgi_file_name = fetch(:wsgi_file_name, 'main.wsgi') 177 | execute "ln -sf #{wsgi_path}/#{wsgi_file_name} #{wsgi_path}/live.wsgi" 178 | end 179 | end 180 | 181 | desc "Run django migrations" 182 | task :migrate do 183 | if fetch(:multidb) 184 | django("sync_all", '--noinput', run_on=:jobs) 185 | else 186 | django("migrate", "--noinput", run_on=:jobs) 187 | end 188 | end 189 | end 190 | 191 | namespace :nodejs do 192 | 193 | desc 'Install node modules' 194 | task :npm_install do 195 | on roles(:web) do 196 | path = fetch(:npm_path) ? File.join(release_path, fetch(:npm_path)) : release_path 197 | within path do 198 | execute 'npm', 'install', fetch(:npm_install_production, '--production') 199 | end 200 | end 201 | end 202 | 203 | desc 'Run npm tasks' 204 | task :npm do 205 | invoke 'nodejs:npm_install' 206 | on roles(:web) do 207 | path = fetch(:npm_path) ? File.join(release_path, fetch(:npm_path)) : release_path 208 | within path do 209 | fetch(:npm_tasks).each do |task, args| 210 | execute "./node_modules/.bin/#{task}", args 211 | end 212 | end 213 | end 214 | end 215 | 216 | end 217 | 218 | 219 | before 'deploy:cleanup', 's3:cleanup' 220 | 221 | namespace :s3 do 222 | 223 | desc 'Clean up old s3 buckets' 224 | task :cleanup do 225 | if fetch(:create_s3_bucket) and fetch(:delete_old_s3_buckets, true) 226 | raw_directories = [] 227 | on roles(:web) do 228 | releases = capture(:ls, '-xtr', releases_path).split 229 | if releases.count >= fetch(:keep_releases) 230 | raw_directories.concat releases.last(fetch(:keep_releases)) 231 | else 232 | raw_directories.concat releases 233 | end 234 | end 235 | directories = raw_directories.uniq 236 | require 'fog' 237 | storage = Fog::Storage.new({ 238 | aws_access_key_id: fetch(:aws_access_key), 239 | aws_secret_access_key: fetch(:aws_secret_key), 240 | provider: "AWS" 241 | }) 242 | buckets = storage.directories.all.select { |b| b.key.start_with? fetch(:s3_bucket_prefix) } 243 | buckets = buckets.select { |b| not directories.include?(b.key.split('-').last) } 244 | buckets.each do |old_bucket| 245 | files = old_bucket.files.map{ |file| file.key } 246 | storage.delete_multiple_objects(old_bucket.key, files) unless files.empty? 247 | storage.delete_bucket(old_bucket.key) 248 | end 249 | end 250 | end 251 | 252 | desc 'Create a new bucket in s3 to deploy static files to' 253 | task :create_bucket do 254 | settings_path = File.join(release_path, fetch(:django_settings_dir)) 255 | s3_settings_path = File.join(settings_path, 's3_settings.py') 256 | bucket_name = "#{fetch(:s3_bucket_prefix)}-#{asset_timestamp.sub('.', '')}" 257 | 258 | on roles(:all) do 259 | execute %Q|echo "STATIC_URL = 'https://s3.amazonaws.com/#{bucket_name}/'" > #{s3_settings_path}| 260 | execute %Q|echo "AWS_ACCESS_KEY_ID = '#{fetch(:aws_access_key)}'" >> #{s3_settings_path}| 261 | execute %Q|echo "AWS_SECRET_ACCESS_KEY = '#{fetch(:aws_secret_key)}'" >> #{s3_settings_path}| 262 | execute %Q|echo "AWS_STORAGE_BUCKET_NAME = '#{bucket_name}'" >> #{s3_settings_path}| 263 | execute %Q|echo 'from .s3_settings import *' >> #{settings_path}/#{fetch(:django_settings)}.py| 264 | execute %Q|echo 'STATICFILES_STORAGE = "storages.backends.s3boto.S3BotoStorage"' >> #{settings_path}/#{fetch(:django_settings)}.py| 265 | end 266 | 267 | require 'fog' 268 | storage = Fog::Storage.new({ 269 | aws_access_key_id: fetch(:aws_access_key), 270 | aws_secret_access_key: fetch(:aws_secret_key), 271 | provider: "AWS" 272 | }) 273 | storage.put_bucket(bucket_name) 274 | storage.put_bucket_policy(bucket_name, { 275 | 'Statement' => [{ 276 | 'Sid' => 'AddPerm', 277 | 'Effect' => 'Allow', 278 | 'Principal' => '*', 279 | 'Action' => ['s3:GetObject'], 280 | 'Resource' => ["arn:aws:s3:::#{bucket_name}/*"] 281 | }] 282 | }) 283 | storage.put_bucket_cors(bucket_name, { 284 | "CORSConfiguration" => [{ 285 | "AllowedOrigin" => ["*"], 286 | "AllowedHeader" => ["*"], 287 | "AllowedMethod" => ["GET"], 288 | "MaxAgeSeconds" => 3000 289 | }] 290 | }) 291 | 292 | end 293 | 294 | end 295 | --------------------------------------------------------------------------------