simplerubysteps 0.0.7 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41e3a9ed5b8d6c32357fa880c2826cdf18a16fb9195260c5d10c7063b7dcb474
4
- data.tar.gz: '07180493d6f6456f41164cd74f1410fb8bcd6649a645d109011c85ec9133c463'
3
+ metadata.gz: 32057d9ea30569964077a231e6e5ed88aff396dabc03151c1ef77bbac237a7e2
4
+ data.tar.gz: 81d13b619c57a3b793ed7039a06e0ec2a730aa8844d70c8603f35277799090e0
5
5
  SHA512:
6
- metadata.gz: 0620c89b84893c3e58078ab8dc66a211aaef5a52101a45e7392bde32bfdab3be3cc9d83bdee6e9adcfd1f3ecdc1e3420dd9068f58bea07550aad1993d0775a90
7
- data.tar.gz: 50f9d29b51926176a2a3edc900694233f38d44511ef5115433cc756f88c74df5ffca90bc924406644580c43e1f9ca6e2b1e2f79187aa0b3c9eca003f14b2daae
6
+ metadata.gz: 68f92e9f3b4e7d0e5249e7df03f97a9e133ab51274117b2538b8672bf8e6221c6c8d2130679395ff0e1e57ee18f6e0bf4c321fe8ce034e0fe14c53ca37150e65
7
+ data.tar.gz: 1185695c5f0bc3c3d093cb7984c2947ec1ef2f4be737586f73ae058d2266ba90121d9d48b37608993877784eee729010867c15754309584beafe88c01d6b5836
data/README.md CHANGED
@@ -1,55 +1,96 @@
1
1
  # Simplerubysteps
2
2
 
3
- Simplerubysteps simplifies the administration of AWS Step Functions with ruby (eventually - this is an early alpha version)
3
+ Simplerubysteps makes it easy to manage AWS Step Functions with ruby.
4
4
 
5
- This is software in the experimental stage and should not really be used by anyone. Be warned.
5
+ * Phase I (we are here): Experimenting and exploring the problem and solution domain. The aim is to explore the DSL capabilities and user experience of the automation tool. Things will work, but may change over time. The released gem versions of Simplerubysteps are early alpha versions and therefore should not be used by anyone in production.
6
+ * Phase II: First release candidate (possibly rewritten from scratch)
7
+ * Phase III: Maintain and evolve
6
8
 
7
- ## Installation
9
+ ## Installation and Usage
8
10
 
9
- Prerequisites (for alpha version):
10
- * AWS CLI installed
11
+ ### Prerequisites
11
12
 
12
- ## Usage
13
+ * AWS CLI installed (mainly for debugging privileges)
14
+ * Configured AWS CLI profile with sufficient permissions to create IAM roles and policies, create Lambda functions, create and run Step Functions state machines, run CloudWatch log queries, etc.
13
15
 
14
- ### Install the gem and the simplerubysteps CLI
16
+ ### Install the gem and the srs CLI
15
17
 
16
18
  ```
17
19
  gem install simplerubysteps
18
20
  ```
19
21
 
20
- ### Create AWS Step Functions State Machine with ruby DSL
22
+ ### Create an AWS Step Function State Machine with the simplerubysteps Ruby DSL
21
23
 
22
24
  ```
23
- cd samples/sample1
25
+ mkdir -p samples/hello-world-2
26
+ cd samples/hello-world-2
27
+
24
28
  vi workflow.rb
25
29
  ```
26
30
 
27
- ### Create CloudFormation stack with Step Functions State Machine and supporting Lambda function
31
+ #### Hello World State Machine (workflow.rb)
28
32
 
29
33
  ```
30
- export AWS_PROFILE=...
31
- cd samples/sample1
32
- simplerubysteps deploy
34
+ require "simplerubysteps"
35
+ include Simplerubysteps
36
+ kind "EXPRESS"
37
+
38
+ GERMAN_WORDS = ["Hallo"]
39
+
40
+ def is_german?(word)
41
+ GERMAN_WORDS.include? word
42
+ end
43
+
44
+ task :start do
45
+ transition_to :german do |data|
46
+ is_german? data["hello"]
47
+ end
48
+
49
+ default_transition_to :english
50
+ end
51
+
52
+ task :german do
53
+ action do |data|
54
+ { hello_world: "#{data["hello"]} Welt!" }
55
+ end
56
+ end
57
+
58
+ task :english do
59
+ action do |data|
60
+ { hello_world: "#{data["hello"]} World!" }
61
+ end
62
+ end
33
63
  ```
34
64
 
35
- ### Trigger State Machine Execution and wait for completion
65
+ ### Deploy the Step Functions State Machine and the Lambda function that implements the Task Actions (with srs deploy)
66
+
67
+ ```
68
+ export AWS_PROFILE=<AWS CLI profile name with sufficient privileges>
69
+ cd samples/hello-world-2
36
70
 
71
+ srs deploy
37
72
  ```
38
- export AWS_PROFILE=...
39
- cd samples/sample1
40
73
 
41
- ./start-directbranch.sh
74
+ ### Trigger State Machine executions (with srs start)
42
75
 
43
- ./sample-task-worker.sh &
44
- ./start-callbackbranch.sh
45
76
  ```
77
+ export AWS_PROFILE=<AWS CLI profile name with sufficient privileges>
78
+ cd samples/hello-world-2
46
79
 
47
- ### Delete CloudFormation stack
80
+ # Bellow: will print "Hello World!"
81
+ echo '{"hello":"Hello"}'|srs start|jq -r ".output"|jq -r ".hello_world"
48
82
 
83
+ # Bellow: will print "Hallo Welt!"
84
+ echo '{"hello":"Hallo"}'|srs start|jq -r ".output"|jq -r ".hello_world"
49
85
  ```
50
- export AWS_PROFILE=...
51
- cd samples/sample1
52
- simplerubysteps destroy
86
+
87
+ ### Delete CloudFormation stack (with srs destroy)
88
+
89
+ ```
90
+ export AWS_PROFILE=<AWS CLI profile name with sufficient privileges>
91
+ cd samples/hello-world-2
92
+
93
+ srs destroy
53
94
  ```
54
95
 
55
96
  ## Development
@@ -58,8 +99,10 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
58
99
 
59
100
  ### TODOs
60
101
 
61
- * Custom IAM policies per Lambda task
62
- * Workflow action unit test support
102
+ * Custom IAM policies per Lambda task (e.g. to allow a task to send a message to an SQS queue)
103
+ * Workflow task/action unit test support
104
+ * Better error handling and reporting
105
+ * Improved stack update strategy (e.g. renamed or added task scenario)
63
106
  * ...
64
107
 
65
108
  ## Contributing
data/exe/simplerubysteps CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- bin_dir = File.expand_path(File.dirname(__FILE__))
4
- include_path = File.join(bin_dir, "../lib/tool.rb")
3
+ require "simplerubysteps"
4
+ require "simplerubysteps/tool"
5
5
 
6
- require include_path
6
+ Simplerubysteps::Tool.new.run
data/exe/srs ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "simplerubysteps"
4
+ require "simplerubysteps/tool"
5
+
6
+ Simplerubysteps::Tool.new.run
data/lib/function.rb CHANGED
@@ -1,14 +1,17 @@
1
- require "json"
1
+ require "simplerubysteps"
2
+
2
3
  require "./workflow.rb"
3
4
 
5
+ include Simplerubysteps
6
+
4
7
  def handler(event:, context:)
5
8
  puts ENV.inspect if ENV["DEBUG"]
6
9
  puts event if ENV["DEBUG"]
7
10
  puts context.inspect if ENV["DEBUG"]
8
11
 
9
12
  if event["Token"]
10
- $sm.states[event["Task"].to_sym].perform_action event["Input"], event["Token"]
13
+ $sm.states[ENV["task"].to_sym].perform_action event["Input"], event["Token"]
11
14
  else
12
- $sm.states[event["Task"].to_sym].perform_action event["Input"]
15
+ $sm.states[ENV["task"].to_sym].perform_action event["Input"]
13
16
  end
14
17
  end
@@ -1,66 +1,53 @@
1
+ require "erb"
2
+
3
+ module Simplerubysteps
4
+ CLOUDFORMATION_ERB_TEMPLATE = <<-YAML
1
5
  ---
2
6
  AWSTemplateFormatVersion: "2010-09-09"
3
- Parameters:
7
+
8
+ <% if resources[:functions] %>
9
+ Parameters:
4
10
  LambdaS3:
5
11
  Description: LambdaS3.
6
- Default: function.zip
7
- Type: String
12
+ Type: String
13
+ <% end %>
14
+
15
+ <% if resources[:state_machine] %>
8
16
  StepFunctionsS3:
9
17
  Description: StepFunctionsS3.
10
- Default: my-state-machine-definition.json
11
- Type: String
12
- DeployLambda:
13
- Description: DeployLambda.
14
- Default: "yes"
15
- Type: String
16
- AllowedValues:
17
- - "yes"
18
- - "no"
19
- ConstraintDescription: must specify yes or no.
20
- DeployStepfunctions:
21
- Description: DeployStepfunctions.
22
- Default: "yes"
23
18
  Type: String
24
- AllowedValues:
25
- - "yes"
26
- - "no"
27
- ConstraintDescription: must specify yes or no.
28
19
  StateMachineType:
29
20
  Description: StateMachineType.
30
- Default: STANDARD
31
- Type: String
32
- Conditions:
33
- CreateLambda: !Equals
34
- - !Ref DeployLambda
35
- - "yes"
36
- CreateStepfunctions: !Equals
37
- - !Ref DeployStepfunctions
38
- - "yes"
21
+ Type: String
22
+ <% end %>
23
+
39
24
  Resources:
40
- DeployBucket:
25
+ DeployBucket:
41
26
  Type: AWS::S3::Bucket
42
- LambdaFunction:
43
- Type: 'AWS::Lambda::Function'
44
- Condition: CreateLambda
27
+
28
+ <% if resources[:functions] %>
29
+ <% resources[:functions].each_with_index do |resource, index| %>
30
+ LambdaFunction<%= index %>:
31
+ Type: "AWS::Lambda::Function"
45
32
  Properties:
46
33
  Code:
47
34
  S3Bucket: !Ref DeployBucket
48
35
  S3Key: !Ref LambdaS3
49
- Handler: function.handler
50
- Role: !GetAtt MyLambdaRole.Arn
36
+ Handler: function.handler
37
+ Role: !GetAtt MyLambdaRole<%= index %>.Arn
51
38
  Runtime: ruby2.7
52
- StepFunctionsStateMachine:
53
- Type: 'AWS::StepFunctions::StateMachine'
54
- Condition: CreateStepfunctions
39
+ Environment:
40
+ Variables:
41
+ <% resource.each do |k, v| %>
42
+ <%= k %>: <%= v %>
43
+ <% end %>
44
+ LogGroup<%= index %>:
45
+ Type: AWS::Logs::LogGroup
55
46
  Properties:
56
- DefinitionS3Location:
57
- Bucket: !Ref DeployBucket
58
- Key: !Ref StepFunctionsS3
59
- RoleArn: !GetAtt StepFunctionsRole.Arn
60
- StateMachineType: !Ref StateMachineType
61
- MyLambdaRole:
47
+ LogGroupName: !Sub "/aws/lambda/${LambdaFunction<%= index %>}"
48
+ RetentionInDays: 7
49
+ MyLambdaRole<%= index %>:
62
50
  Type: AWS::IAM::Role
63
- Condition: CreateLambda
64
51
  Properties:
65
52
  AssumeRolePolicyDocument:
66
53
  Version: "2012-10-17"
@@ -81,9 +68,20 @@ Resources:
81
68
  - logs:CreateLogStream
82
69
  - logs:PutLogEvents
83
70
  Resource: arn:aws:logs:*:*:*
71
+ <% end %>
72
+ <% end %>
73
+
74
+ <% if resources[:state_machine] %>
75
+ StepFunctionsStateMachine:
76
+ Type: "AWS::StepFunctions::StateMachine"
77
+ Properties:
78
+ DefinitionS3Location:
79
+ Bucket: !Ref DeployBucket
80
+ Key: !Ref StepFunctionsS3
81
+ RoleArn: !GetAtt StepFunctionsRole.Arn
82
+ StateMachineType: !Ref StateMachineType
84
83
  StepFunctionsRole:
85
84
  Type: AWS::IAM::Role
86
- Condition: CreateStepfunctions
87
85
  Properties:
88
86
  AssumeRolePolicyDocument:
89
87
  Version: "2012-10-17"
@@ -106,22 +104,41 @@ Resources:
106
104
  - Effect: Allow
107
105
  Action:
108
106
  - lambda:InvokeFunction
109
- Resource: !GetAtt LambdaFunction.Arn
107
+ Resource:
108
+ <% resources[:functions].each_with_index do |resource, index| %>
109
+ - !GetAtt LambdaFunction<%= index %>.Arn
110
+ <% end %>
111
+ <% end %>
112
+
110
113
  Outputs:
111
114
  DeployBucket:
112
115
  Value: !Ref DeployBucket
113
- LambdaRoleARN:
114
- Value: !GetAtt MyLambdaRole.Arn
115
- Condition: CreateLambda
116
- LambdaFunctionARN:
117
- Value: !GetAtt LambdaFunction.Arn
118
- Condition: CreateLambda
119
- LambdaFunctionName:
120
- Value: !Ref LambdaFunction
121
- Condition: CreateLambda
122
- StepFunctionsRoleARN:
123
- Value: !GetAtt StepFunctionsRole.Arn
124
- Condition: CreateStepfunctions
116
+
117
+ <% if resources[:functions] %>
118
+ LambdaCount:
119
+ Value: <%= resources[:functions].length %>
120
+ <% resources[:functions].each_with_index do |resource, index| %>
121
+ LambdaRoleARN<%= index %>:
122
+ Value: !GetAtt MyLambdaRole<%= index %>.Arn
123
+ LambdaFunctionARN<%= index %>:
124
+ Value: !GetAtt LambdaFunction<%= index %>.Arn
125
+ LambdaFunctionName<%= index %>:
126
+ Value: !Ref LambdaFunction<%= index %>
127
+ <% end %>
128
+ <% end %>
129
+
130
+ <% if resources[:state_machine] %>
125
131
  StepFunctionsStateMachineARN:
126
132
  Value: !GetAtt StepFunctionsStateMachine.Arn
127
- Condition: CreateStepfunctions
133
+ StateMachineType:
134
+ Value: !Ref StateMachineType
135
+ StepFunctionsRoleARN:
136
+ Value: !GetAtt StepFunctionsRole.Arn
137
+ <% end %>
138
+ YAML
139
+
140
+ def self.cloudformation_yaml(resources)
141
+ template = ERB.new(CLOUDFORMATION_ERB_TEMPLATE)
142
+ template.result(binding)
143
+ end
144
+ end
@@ -0,0 +1,86 @@
1
+ require "json"
2
+
3
+ module Simplerubysteps
4
+ $sm = StateMachine.new
5
+ $tasks = []
6
+
7
+ def kind(k)
8
+ $sm.kind = k
9
+ end
10
+
11
+ def task(name)
12
+ t = $sm.add Task.new(name)
13
+
14
+ $tasks.last.next = t if $tasks.last
15
+
16
+ $tasks.push t
17
+ yield if block_given?
18
+ $tasks.pop
19
+ end
20
+
21
+ def callback(name)
22
+ t = $sm.add Callback.new(name)
23
+
24
+ $tasks.last.next = t if $tasks.last
25
+
26
+ $tasks.push t
27
+ yield if block_given?
28
+ $tasks.pop
29
+ end
30
+
31
+ def action(&action_block)
32
+ $tasks.last.action &action_block
33
+ end
34
+
35
+ def transition(state)
36
+ $tasks.last.next = state
37
+ end
38
+
39
+ def transition_to(state, &condition_block)
40
+ choice = $tasks.last.implicit_choice
41
+
42
+ c = ChoiceItem.new({
43
+ :Variable => "$.#{choice.name}_#{state}",
44
+ :StringMatches => "yes",
45
+ })
46
+ c.next = state
47
+ c.implicit_condition_block = condition_block
48
+
49
+ choice.add c
50
+ end
51
+
52
+ def default_transition_to(state)
53
+ choice = $tasks.last.implicit_choice
54
+
55
+ choice.default = state
56
+ end
57
+
58
+ def choice(name)
59
+ t = $sm.add Choice.new(name)
60
+
61
+ $tasks.last.next = t if $tasks.last
62
+
63
+ $tasks.push t
64
+ yield if block_given?
65
+ $tasks.pop
66
+ end
67
+
68
+ def string_matches(var, match)
69
+ c = ChoiceItem.new({
70
+ :Variable => var,
71
+ :StringMatches => match,
72
+ })
73
+
74
+ $tasks.last.add c
75
+
76
+ $tasks.push c
77
+ yield if block_given?
78
+ $tasks.pop
79
+ end
80
+
81
+ def default
82
+ $tasks.push $tasks.last
83
+ yield if block_given?
84
+ $tasks.pop
85
+ end
86
+ end
@@ -0,0 +1,204 @@
1
+ module Simplerubysteps
2
+ $LAMBDA_FUNCTION_ARNS = ENV["LAMBDA_FUNCTION_ARNS"] ? ENV["LAMBDA_FUNCTION_ARNS"].split(",") : nil
3
+
4
+ def pop_function_arn
5
+ return "unknown" unless $LAMBDA_FUNCTION_ARNS
6
+ arn = $LAMBDA_FUNCTION_ARNS.first
7
+ $LAMBDA_FUNCTION_ARNS.delete arn
8
+ arn
9
+ end
10
+
11
+ def pop_function_name
12
+ return "unknown" unless pop_function_arn =~ /.+\:function\:(.+)/
13
+ $1
14
+ end
15
+
16
+ class StateMachine
17
+ attr_reader :states
18
+ attr_reader :start_at
19
+ attr_accessor :kind
20
+
21
+ def initialize()
22
+ @states = {}
23
+ @kind = "STANDARD"
24
+ end
25
+
26
+ def add(state)
27
+ @start_at = state.name unless @start_at
28
+
29
+ @states[state.name] = state
30
+ state.state_machine = self
31
+
32
+ state
33
+ end
34
+
35
+ def cloudformation_config
36
+ data = []
37
+ states.each do |name, state|
38
+ if state.is_a? Task or state.is_a? Callback
39
+ data.push({
40
+ task: name,
41
+ })
42
+ end
43
+ end
44
+ data
45
+ end
46
+
47
+ def render
48
+ {
49
+ :StartAt => @start_at,
50
+ :States => @states.map { |name, state| [name, state.render] }.to_h,
51
+ }
52
+ end
53
+ end
54
+
55
+ class State
56
+ attr_reader :name
57
+ attr_accessor :state_machine
58
+
59
+ def initialize(name)
60
+ @name = name
61
+ @dict = {}
62
+ end
63
+
64
+ def []=(key, value)
65
+ @dict[key] = value
66
+ end
67
+
68
+ def next=(state)
69
+ @dict[:Next] = (state.is_a? Symbol) ? state : state.name
70
+ end
71
+
72
+ def render
73
+ dict = @dict
74
+ dict[:End] = true unless dict[:Next]
75
+ dict
76
+ end
77
+ end
78
+
79
+ class Task < State
80
+ def initialize(name)
81
+ super
82
+ @dict[:Type] = "Task"
83
+ @dict[:Resource] = pop_function_arn
84
+ @dict[:Parameters] = {
85
+ :Task => name,
86
+ "Input.$" => "$",
87
+ }
88
+ end
89
+
90
+ def action(&action_block)
91
+ @action_block = action_block
92
+ end
93
+
94
+ def perform_action(input)
95
+ output = input # default: pass through
96
+
97
+ output = @action_block.call(input) if @action_block
98
+
99
+ if @implicit_choice
100
+ output = {} unless output
101
+ @implicit_choice.perform_action(output)
102
+ end
103
+
104
+ output
105
+ end
106
+
107
+ def implicit_choice
108
+ unless @implicit_choice
109
+ @implicit_choice = Choice.new("#{name}_choice")
110
+ $sm.add @implicit_choice
111
+ self.next = @implicit_choice
112
+ end
113
+ @implicit_choice
114
+ end
115
+ end
116
+
117
+ class Callback < State
118
+ def initialize(name)
119
+ super
120
+ @dict[:Type] = "Task"
121
+ @dict[:Resource] = "arn:aws:states:::lambda:invoke.waitForTaskToken"
122
+ @dict[:Parameters] = {
123
+ :FunctionName => pop_function_name,
124
+ :Payload => {
125
+ :Task => name,
126
+ "Input.$" => "$",
127
+ "Token.$" => "$$.Task.Token",
128
+ },
129
+ }
130
+ end
131
+
132
+ def action(&action_block)
133
+ @action_block = action_block
134
+ end
135
+
136
+ def perform_action(input, token)
137
+ @action_block.call(input, token) if @action_block
138
+ end
139
+ end
140
+
141
+ class ChoiceItem
142
+ attr_accessor :implicit_condition_block
143
+
144
+ def initialize(dict = {}, state = nil)
145
+ @dict = dict
146
+ self.next = state if state
147
+ end
148
+
149
+ def next=(state)
150
+ @dict[:Next] = (state.is_a? Symbol) ? state : state.name
151
+ end
152
+
153
+ def render
154
+ @dict
155
+ end
156
+
157
+ def perform_action(choice_name, output)
158
+ if @implicit_condition_block
159
+ output["#{choice_name}_#{@dict[:Next]}"] = @implicit_condition_block.call(output) ? "yes" : "no"
160
+ end
161
+ end
162
+ end
163
+
164
+ class Choice < State
165
+ attr_reader :choices
166
+
167
+ def initialize(name)
168
+ super
169
+ @choices = []
170
+ @dict[:Type] = "Choice"
171
+ end
172
+
173
+ def add(item)
174
+ @choices.push item
175
+ end
176
+
177
+ def add_string_matches(var, match, state)
178
+ add ChoiceItem.new({
179
+ :Variable => var,
180
+ :StringMatches => match,
181
+ }, state)
182
+ end
183
+
184
+ def default=(state)
185
+ @dict[:Default] = (state.is_a? Symbol) ? state : state.name
186
+ end
187
+
188
+ def next=(state)
189
+ self.default = state
190
+ end
191
+
192
+ def render
193
+ dict = @dict.clone
194
+ dict[:Choices] = @choices.map { |item| item.render }
195
+ dict
196
+ end
197
+
198
+ def perform_action(output)
199
+ @choices.each do |choice|
200
+ choice.perform_action name, output
201
+ end
202
+ end
203
+ end
204
+ end