middleman-aws-deploy 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +2 -0
- data/README.md +38 -0
- data/lib/middleman-aws-deploy.rb +6 -0
- data/lib/middleman-aws-deploy/cloudfront-invalidate.rb +98 -0
- data/lib/middleman-aws-deploy/s3deploy.rb +98 -0
- data/lib/middleman-aws-deploy/version.rb +5 -0
- data/middleman-aws-deploy.gemspec +26 -0
- metadata +133 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# Middleman AWS Deploy
|
2
|
+
|
3
|
+
Deploys build files to S3 at the end of the Middleman build toolchain, and/or invalidates Cloudfront.
|
4
|
+
|
5
|
+
In your `config.rb` file, add.
|
6
|
+
|
7
|
+
```
|
8
|
+
configure :build do
|
9
|
+
# ...
|
10
|
+
if ENV.include?('DEPLOY')
|
11
|
+
activate :s3_deploy do |s3|
|
12
|
+
s3.access_key_id = ENV['AWS_ACCESS_KEY_ID']
|
13
|
+
s3.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
|
14
|
+
s3.bucket = ENV['AWS_S3_BUCKET']
|
15
|
+
end
|
16
|
+
activate :invalidate_cloudfront do |cf|
|
17
|
+
cf.access_key_id = ENV['AWS_ACCESS_KEY_ID']
|
18
|
+
cf.secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
|
19
|
+
cf.distribution_id = ENV['AWS_CLOUDFRONT_DIST_ID']
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
```
|
24
|
+
|
25
|
+
With valid AWS creds, an s3 bucket, and cloudfront distribution id, you can deploy
|
26
|
+
with the env var DEPLOY as a trigger (you can use whichever kind of trigger you like):
|
27
|
+
|
28
|
+
```
|
29
|
+
DEPLOY=true middleman build
|
30
|
+
```
|
31
|
+
|
32
|
+
### S3 Deployment Note
|
33
|
+
|
34
|
+
The S3 deployer will check local file's md5 hash against the remote s3 etag. If
|
35
|
+
they are the same, it will not upload the identical file. However, if
|
36
|
+
you have `:cachebuster` set to active the hashes will always end up different
|
37
|
+
on every build, triggering an upload. There's little such danger however for
|
38
|
+
assets (css, images, etc).
|
@@ -0,0 +1,6 @@
|
|
1
|
+
require "middleman-core"
|
2
|
+
require "middleman-aws-deploy/s3deploy"
|
3
|
+
require "middleman-aws-deploy/cloudfront-invalidate"
|
4
|
+
|
5
|
+
::Middleman::Extensions.register(:s3_deploy, Middleman::AWSDeploy::S3Deploy)
|
6
|
+
::Middleman::Extensions.register(:invalidate_cloudfront, Middleman::AWSDeploy::InvalidateCloudfront)
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'uri'
|
3
|
+
require 'hmac'
|
4
|
+
require 'hmac-sha1'
|
5
|
+
require 'net/https'
|
6
|
+
require 'base64'
|
7
|
+
|
8
|
+
CF_BATCH_SIZE = 1000
|
9
|
+
|
10
|
+
module Middleman
|
11
|
+
module AWSDeploy
|
12
|
+
module InvalidateCloudfront
|
13
|
+
Opts = Struct.new(:access_key_id, :secret_access_key, :distribution_id)
|
14
|
+
|
15
|
+
class << self
|
16
|
+
module Options
|
17
|
+
def aws_cfi_opts
|
18
|
+
::Middleman::AWSDeploy::InvalidateCloudfront.options
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def options
|
23
|
+
@@options
|
24
|
+
end
|
25
|
+
|
26
|
+
def registered(app, opts_data = {}, &block)
|
27
|
+
@@options = Opts.new(opts_data)
|
28
|
+
yield @@options if block_given?
|
29
|
+
|
30
|
+
app.send :include, Options
|
31
|
+
app.after_build do
|
32
|
+
puts "== Invalidating CloudFront"
|
33
|
+
|
34
|
+
# you can only invalidate in batches of 1000
|
35
|
+
def invalidate(files, pos=0)
|
36
|
+
paths, count = "", 0
|
37
|
+
|
38
|
+
escaped_files = []
|
39
|
+
files[pos...(pos+CF_BATCH_SIZE)].each do |key|
|
40
|
+
count += 1
|
41
|
+
escaped_files << (escaped_file = URI.escape(key.to_s))
|
42
|
+
paths += "<Path>#{escaped_file}</Path>"
|
43
|
+
end
|
44
|
+
|
45
|
+
return if count < 1
|
46
|
+
|
47
|
+
paths = "<Paths><Quantity>#{count}</Quantity><Items>#{paths}</Items></Paths>"
|
48
|
+
|
49
|
+
digest = HMAC::SHA1.new(aws_cfi_opts.secret_access_key)
|
50
|
+
digest << date = Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S %Z")
|
51
|
+
|
52
|
+
uri = URI.parse("https://cloudfront.amazonaws.com/2012-07-01/distribution/#{aws_cfi_opts.distribution_id}/invalidation")
|
53
|
+
header = {
|
54
|
+
'x-amz-date' => date,
|
55
|
+
'Content-Type' => 'text/xml',
|
56
|
+
'Authorization' => "AWS %s:%s" %
|
57
|
+
[aws_cfi_opts.access_key_id, Base64.encode64(digest.digest)]
|
58
|
+
}
|
59
|
+
req = Net::HTTP::Post.new(uri.path)
|
60
|
+
req.initialize_http_header(header)
|
61
|
+
body = "<InvalidationBatch xmlns=\"http://cloudfront.amazonaws.com/doc/2012-07-01/\">#{paths}<CallerReference>ref_#{Time.now.utc.to_i}</CallerReference></InvalidationBatch>"
|
62
|
+
req.body = body
|
63
|
+
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
65
|
+
http.use_ssl = true
|
66
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
67
|
+
res = http.request(req)
|
68
|
+
if res.code == '201'
|
69
|
+
puts "CloudFront reloading #{count} paths"
|
70
|
+
else
|
71
|
+
$stderr.puts "CloudFront Invalidate failed with error #{res.code}"
|
72
|
+
$stderr.puts res.body
|
73
|
+
$stderr.puts escaped_files.join("\n")
|
74
|
+
end
|
75
|
+
return if res.code == 400
|
76
|
+
|
77
|
+
if count == CF_BATCH_SIZE
|
78
|
+
invalidate(files, pos+count)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def cf_files
|
83
|
+
files = Dir["./build/**/.*", "./build/**/*"]
|
84
|
+
.reject{|f| File.directory?(f) }.map{|f| f.sub(/^(?:\.\/)?build\//, '/') }
|
85
|
+
# if :directory_indexes is active, we must invalidate both files and dirs
|
86
|
+
# doing this 100% of the time, it would be nice if we could find of
|
87
|
+
files += files.map{|f| f.gsub(/\/index\.html$/, '/') }
|
88
|
+
files.uniq!
|
89
|
+
end
|
90
|
+
|
91
|
+
invalidate(cf_files)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
alias :included :registered
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'fog'
|
3
|
+
require 'parallel'
|
4
|
+
require 'progressbar'
|
5
|
+
require 'digest/md5'
|
6
|
+
|
7
|
+
module Middleman
|
8
|
+
module AWSDeploy
|
9
|
+
module S3Deploy
|
10
|
+
Opts = Struct.new(:access_key_id, :secret_access_key, :threads, :bucket, :region)
|
11
|
+
|
12
|
+
class << self
|
13
|
+
module Options
|
14
|
+
def aws_s3_opts
|
15
|
+
::Middleman::AWSDeploy::S3Deploy.options
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def options
|
20
|
+
@@options
|
21
|
+
end
|
22
|
+
|
23
|
+
def registered(app, opts_data = {}, &block)
|
24
|
+
@@options = Opts.new(opts_data)
|
25
|
+
yield @@options if block_given?
|
26
|
+
|
27
|
+
@@options.threads ||= 8
|
28
|
+
app.send :include, Options
|
29
|
+
app.after_build do
|
30
|
+
puts "== Uploading to S3"
|
31
|
+
|
32
|
+
def storage
|
33
|
+
@storage ||= Fog::Storage.new({
|
34
|
+
:provider => 'AWS',
|
35
|
+
:aws_access_key_id => aws_s3_opts.access_key_id,
|
36
|
+
:aws_secret_access_key => aws_s3_opts.secret_access_key,
|
37
|
+
:region => aws_s3_opts.region
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
41
|
+
def remote_files
|
42
|
+
@directory ||= storage.directories.get(aws_s3_opts.bucket)
|
43
|
+
@remote_files ||= @directory.files
|
44
|
+
end
|
45
|
+
|
46
|
+
def etag(key)
|
47
|
+
object = remote_files.head(key)
|
48
|
+
object && object.etag
|
49
|
+
end
|
50
|
+
|
51
|
+
def upload(key)
|
52
|
+
begin
|
53
|
+
remote_files.new({
|
54
|
+
:key => key,
|
55
|
+
:body => File.open("./build/#{key}"),
|
56
|
+
:public => true,
|
57
|
+
:acl => 'public-read'
|
58
|
+
}).save
|
59
|
+
rescue
|
60
|
+
$stderr.puts "Failed to upload #{key}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def files
|
65
|
+
@files ||= Dir["./build/**/.*", "./build/**/*"]
|
66
|
+
.reject{|f| File.directory?(f) }.map{|f| f.sub(/^(?:\.\/)?build\//, '') }
|
67
|
+
end
|
68
|
+
|
69
|
+
# no need to upload an exact copy of an existing remote file
|
70
|
+
replace = []
|
71
|
+
progress = ProgressBar.new('Hash check', files.count)
|
72
|
+
Parallel.each(files,
|
73
|
+
:in_threads => aws_s3_opts.threads,
|
74
|
+
:finish => lambda {|i, item| progress.inc }) do |f|
|
75
|
+
md5 = Digest::MD5.hexdigest(File.read("./build/#{f}"))
|
76
|
+
replace << f if md5 != etag(f)
|
77
|
+
end
|
78
|
+
progress.finish
|
79
|
+
|
80
|
+
if replace.empty?
|
81
|
+
puts "Nothing to upload"
|
82
|
+
else
|
83
|
+
# upload necessary files
|
84
|
+
progress = ProgressBar.new('Uploading', replace.count)
|
85
|
+
Parallel.each(replace,
|
86
|
+
:in_threads => aws_s3_opts.threads,
|
87
|
+
:finish => lambda {|i, item| progress.inc }) do |f|
|
88
|
+
upload(f)
|
89
|
+
end
|
90
|
+
progress.finish
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
alias :included :registered
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "middleman-aws-deploy/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "middleman-aws-deploy"
|
7
|
+
s.version = Middleman::AWSDeploy::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Eric Redmond"]
|
10
|
+
s.email = ["eric.redmond@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/coderoshi/middleman-aws-deploy"
|
12
|
+
s.summary = %q{Deploy to AWS with Middleman}
|
13
|
+
s.description = %q{Adds S3 deploy and Cloudfront invalidation support to the Middleman build toolchain}
|
14
|
+
|
15
|
+
s.rubyforge_project = "middleman-aws-deploy"
|
16
|
+
|
17
|
+
s.files = `git ls-files -z`.split("\0")
|
18
|
+
s.test_files = `git ls-files -z -- {fixtures,features}/*`.split("\0")
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency("middleman-core", ["~> 3.0.0"])
|
22
|
+
s.add_dependency("ruby-hmac", ["~> 0.4.0"])
|
23
|
+
s.add_dependency("parallel", ["~> 0.6.1"])
|
24
|
+
s.add_dependency("fog", ["~> 1.8.0"])
|
25
|
+
s.add_dependency("progressbar", ["~> 0.12.0"])
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: middleman-aws-deploy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.5.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eric Redmond
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-09 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: middleman-core
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.0.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.0.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: ruby-hmac
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.4.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.4.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: parallel
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.6.1
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.6.1
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: fog
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.8.0
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.8.0
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: progressbar
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.12.0
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.12.0
|
94
|
+
description: Adds S3 deploy and Cloudfront invalidation support to the Middleman build
|
95
|
+
toolchain
|
96
|
+
email:
|
97
|
+
- eric.redmond@gmail.com
|
98
|
+
executables: []
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files: []
|
101
|
+
files:
|
102
|
+
- Gemfile
|
103
|
+
- README.md
|
104
|
+
- lib/middleman-aws-deploy.rb
|
105
|
+
- lib/middleman-aws-deploy/cloudfront-invalidate.rb
|
106
|
+
- lib/middleman-aws-deploy/s3deploy.rb
|
107
|
+
- lib/middleman-aws-deploy/version.rb
|
108
|
+
- middleman-aws-deploy.gemspec
|
109
|
+
homepage: https://github.com/coderoshi/middleman-aws-deploy
|
110
|
+
licenses: []
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
require_paths:
|
114
|
+
- lib
|
115
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ! '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
122
|
+
none: false
|
123
|
+
requirements:
|
124
|
+
- - ! '>='
|
125
|
+
- !ruby/object:Gem::Version
|
126
|
+
version: '0'
|
127
|
+
requirements: []
|
128
|
+
rubyforge_project: middleman-aws-deploy
|
129
|
+
rubygems_version: 1.8.24
|
130
|
+
signing_key:
|
131
|
+
specification_version: 3
|
132
|
+
summary: Deploy to AWS with Middleman
|
133
|
+
test_files: []
|