openstax_aws 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.travis.yml +12 -0
  4. data/CHANGELOG.md +11 -0
  5. data/Gemfile +6 -0
  6. data/Gemfile.lock +120 -0
  7. data/LICENSE.txt +1 -0
  8. data/README.md +927 -0
  9. data/Rakefile +6 -0
  10. data/TODO.md +1 -0
  11. data/assets/secrets_sequence_diagram.png +0 -0
  12. data/bin/console +14 -0
  13. data/bin/create_development_environment +26 -0
  14. data/bin/get_latest_ubuntu_ami +31 -0
  15. data/bin/setup +8 -0
  16. data/bin/templates/aws_ruby_development.yml +221 -0
  17. data/examples/deployment.rb +90 -0
  18. data/ideas.md +15 -0
  19. data/lib/openstax/aws/auto_scaling_group.rb +28 -0
  20. data/lib/openstax/aws/auto_scaling_instance.rb +96 -0
  21. data/lib/openstax/aws/build_image_command_1.rb +53 -0
  22. data/lib/openstax/aws/change_set.rb +100 -0
  23. data/lib/openstax/aws/deployment_base.rb +372 -0
  24. data/lib/openstax/aws/distribution.rb +56 -0
  25. data/lib/openstax/aws/ec2_instance_data.rb +18 -0
  26. data/lib/openstax/aws/extensions.rb +19 -0
  27. data/lib/openstax/aws/git_helper.rb +18 -0
  28. data/lib/openstax/aws/image.rb +34 -0
  29. data/lib/openstax/aws/msk_cluster.rb +19 -0
  30. data/lib/openstax/aws/packer_1_2_5.rb +63 -0
  31. data/lib/openstax/aws/packer_1_4_1.rb +72 -0
  32. data/lib/openstax/aws/packer_factory.rb +25 -0
  33. data/lib/openstax/aws/rds_instance.rb +25 -0
  34. data/lib/openstax/aws/s3_text_file.rb +50 -0
  35. data/lib/openstax/aws/sam_stack.rb +85 -0
  36. data/lib/openstax/aws/secrets.rb +302 -0
  37. data/lib/openstax/aws/secrets_factory.rb +126 -0
  38. data/lib/openstax/aws/secrets_set.rb +21 -0
  39. data/lib/openstax/aws/secrets_specification.rb +68 -0
  40. data/lib/openstax/aws/stack.rb +465 -0
  41. data/lib/openstax/aws/stack_event.rb +28 -0
  42. data/lib/openstax/aws/stack_factory.rb +153 -0
  43. data/lib/openstax/aws/stack_parameters.rb +19 -0
  44. data/lib/openstax/aws/stack_status.rb +125 -0
  45. data/lib/openstax/aws/system.rb +21 -0
  46. data/lib/openstax/aws/tag.rb +31 -0
  47. data/lib/openstax/aws/template.rb +129 -0
  48. data/lib/openstax/aws/version.rb +5 -0
  49. data/lib/openstax/aws/wait_message.rb +20 -0
  50. data/lib/openstax_aws.rb +154 -0
  51. data/openstax_aws.gemspec +58 -0
  52. metadata +350 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a6f32242082668d657cf21a1344ac2bd5420988967cdb77a2d58a25bfc4af05d
4
+ data.tar.gz: 2dbfb47ea6aaaec7582f6f62f7c98b2062dae791bc49bd10b1c343d18b9ed9bb
5
+ SHA512:
6
+ metadata.gz: 1a6b20b2acc14b976fa4c76e3588aa34cb0ad81cefc21ed9b90402b4c9dbd73b8f0b652313cc606852ab4dbcd996d19ad939e111bc543e975cb45fff53d749c5
7
+ data.tar.gz: 7e351d427170af5cb02d814cd79bcc675e2e4cb6174d2a598618cae647a524b84f46ba0a140d1f9ef9966d1cd0477f91224f7c956fbf7887209825c6d5625e86
@@ -0,0 +1,19 @@
1
+ *.retry
2
+ *.pem
3
+ *.DS_Store
4
+
5
+ /.bundle/
6
+ /.yardoc
7
+ /_yardoc/
8
+ /coverage/
9
+ /doc/
10
+ /pkg/
11
+ /spec/reports/
12
+ /tmp/
13
+
14
+ # rspec failure tracking
15
+ .rspec_status
16
+
17
+ *.byebug_history
18
+
19
+ .env
@@ -0,0 +1,12 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.6
5
+ cache: bundler
6
+ bundler_args: --retry=6
7
+ script:
8
+ - bundle exec rake
9
+ notifications:
10
+ email: false
11
+ before_install:
12
+ - gem install bundler
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [1.0.0] - 2020-10-03
10
+
11
+ First official version.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in a15k_deployments.gemspec
6
+ gemspec
@@ -0,0 +1,120 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ openstax_aws (1.0.0)
5
+ activesupport
6
+ aws-sdk-autoscaling (~> 1)
7
+ aws-sdk-cloudformation (~> 1)
8
+ aws-sdk-cloudfront (~> 1)
9
+ aws-sdk-ec2 (~> 1)
10
+ aws-sdk-kafka (~> 1)
11
+ aws-sdk-rds (~> 1)
12
+ aws-sdk-s3 (~> 1)
13
+ aws-sdk-ssm (~> 1)
14
+ git
15
+
16
+ GEM
17
+ remote: https://rubygems.org/
18
+ specs:
19
+ activesupport (6.0.3.3)
20
+ concurrent-ruby (~> 1.0, >= 1.0.2)
21
+ i18n (>= 0.7, < 2)
22
+ minitest (~> 5.1)
23
+ tzinfo (~> 1.1)
24
+ zeitwerk (~> 2.2, >= 2.2.2)
25
+ addressable (2.7.0)
26
+ public_suffix (>= 2.0.2, < 5.0)
27
+ awesome_print (1.8.0)
28
+ aws-eventstream (1.1.0)
29
+ aws-partitions (1.380.0)
30
+ aws-sdk-autoscaling (1.47.0)
31
+ aws-sdk-core (~> 3, >= 3.109.0)
32
+ aws-sigv4 (~> 1.1)
33
+ aws-sdk-cloudformation (1.44.0)
34
+ aws-sdk-core (~> 3, >= 3.109.0)
35
+ aws-sigv4 (~> 1.1)
36
+ aws-sdk-cloudfront (1.43.0)
37
+ aws-sdk-core (~> 3, >= 3.109.0)
38
+ aws-sigv4 (~> 1.1)
39
+ aws-sdk-core (3.109.0)
40
+ aws-eventstream (~> 1, >= 1.0.2)
41
+ aws-partitions (~> 1, >= 1.239.0)
42
+ aws-sigv4 (~> 1.1)
43
+ jmespath (~> 1.0)
44
+ aws-sdk-ec2 (1.198.0)
45
+ aws-sdk-core (~> 3, >= 3.109.0)
46
+ aws-sigv4 (~> 1.1)
47
+ aws-sdk-kafka (1.29.0)
48
+ aws-sdk-core (~> 3, >= 3.109.0)
49
+ aws-sigv4 (~> 1.1)
50
+ aws-sdk-kms (1.39.0)
51
+ aws-sdk-core (~> 3, >= 3.109.0)
52
+ aws-sigv4 (~> 1.1)
53
+ aws-sdk-rds (1.102.0)
54
+ aws-sdk-core (~> 3, >= 3.109.0)
55
+ aws-sigv4 (~> 1.1)
56
+ aws-sdk-s3 (1.83.0)
57
+ aws-sdk-core (~> 3, >= 3.109.0)
58
+ aws-sdk-kms (~> 1)
59
+ aws-sigv4 (~> 1.1)
60
+ aws-sdk-ssm (1.93.0)
61
+ aws-sdk-core (~> 3, >= 3.109.0)
62
+ aws-sigv4 (~> 1.1)
63
+ aws-sigv4 (1.2.2)
64
+ aws-eventstream (~> 1, >= 1.0.2)
65
+ byebug (11.1.1)
66
+ concurrent-ruby (1.1.7)
67
+ crack (0.4.3)
68
+ safe_yaml (~> 1.0.0)
69
+ diff-lcs (1.3)
70
+ dotenv (2.7.5)
71
+ git (1.7.0)
72
+ rchardet (~> 1.8)
73
+ hashdiff (1.0.1)
74
+ i18n (1.8.5)
75
+ concurrent-ruby (~> 1.0)
76
+ jmespath (1.4.0)
77
+ minitest (5.14.2)
78
+ public_suffix (4.0.4)
79
+ rake (13.0.1)
80
+ rchardet (1.8.0)
81
+ rspec (3.9.0)
82
+ rspec-core (~> 3.9.0)
83
+ rspec-expectations (~> 3.9.0)
84
+ rspec-mocks (~> 3.9.0)
85
+ rspec-core (3.9.1)
86
+ rspec-support (~> 3.9.1)
87
+ rspec-expectations (3.9.1)
88
+ diff-lcs (>= 1.2.0, < 2.0)
89
+ rspec-support (~> 3.9.0)
90
+ rspec-mocks (3.9.1)
91
+ diff-lcs (>= 1.2.0, < 2.0)
92
+ rspec-support (~> 3.9.0)
93
+ rspec-support (3.9.2)
94
+ safe_yaml (1.0.5)
95
+ thread_safe (0.3.6)
96
+ tzinfo (1.2.7)
97
+ thread_safe (~> 0.1)
98
+ vcr (5.1.0)
99
+ webmock (3.8.3)
100
+ addressable (>= 2.3.6)
101
+ crack (>= 0.3.2)
102
+ hashdiff (>= 0.4.0, < 2.0.0)
103
+ zeitwerk (2.4.0)
104
+
105
+ PLATFORMS
106
+ ruby
107
+
108
+ DEPENDENCIES
109
+ awesome_print
110
+ bundler (~> 1.16)
111
+ byebug
112
+ dotenv
113
+ openstax_aws!
114
+ rake (~> 13.0)
115
+ rspec (~> 3.0)
116
+ vcr
117
+ webmock
118
+
119
+ BUNDLED WITH
120
+ 1.17.3
@@ -0,0 +1 @@
1
+ MIT
@@ -0,0 +1,927 @@
1
+ # aws-ruby
2
+
3
+ [![Build Status](https://travis-ci.org/openstax/aws-ruby.svg?branch=master)](https://travis-ci.org/openstax/aws-ruby)
4
+
5
+ The `openstax_aws` gem helps you deploy your applications to AWS using CloudFormation. It provides a layer on top of
6
+ the AWS SDKs to help coordinate common deployment steps and configurations.
7
+
8
+ The gem also includes utilities that use Packer and Ansible to help you build Amazon Machine Images (AMIs).
9
+
10
+ When using CloudFormation directly, you write template files that define AWS resources and their interconnections. You
11
+ then make AWS API calls (either using the AWS CLI directly or one of the AWS SDKs) that tell AWS to build the resources
12
+ defined in those templates. The resulting set of resources is called a stack. The API calls involved are numerous and
13
+ repetitive.
14
+
15
+ In this gem, you don't make the AWS API calls directly. Instead, you make "deployment" objects that organize and act on
16
+ sets of "stack" objects. While not required to benefit from this gem, DSLs, convenience methods, and conventions let you
17
+ adopt a "convention over configuration" approach to dry up your code.
18
+
19
+ ## Requirements
20
+
21
+ * `git`
22
+
23
+ ## A Quick Look
24
+
25
+ Let's assume we have a web API application defined in two CloudFormation templates: `app.yml` for the web server template
26
+ and `network.yml` for a template that sets up VPCs, subnets, etc. The `app.yml` template defines one `ImageId` parameter
27
+ that is the AMI ID for the web server code and also connects to the network template by importing some of its values.
28
+ There is one Ruby file defining the deployment object, `web_api_deployment.rb`. And then there's an executable script to
29
+ call a method on the deployment, `create_deployment.rb`. Though not required, let's further assume that all files are in
30
+ the same directory:
31
+
32
+ ```
33
+ /myapp $> ls
34
+ app.yml
35
+ network.yml
36
+ web_api_deployment.rb
37
+ create_deployment.rb
38
+ ```
39
+
40
+ ```ruby
41
+ # web_api_deployment.rb
42
+
43
+ class WebApiDeployment < OpenStax::Aws::DeploymentBase
44
+ template_directory __dir__
45
+
46
+ stack :network
47
+ stack :app
48
+
49
+ def initialize(env_name:, region:, dry_run: true)
50
+ super(name: "web-api", env_name: env_name, region: region, dry_run: dry_run)
51
+ end
52
+
53
+ def create(api_image_id)
54
+ network_stack.create(wait: true)
55
+ app_stack.create(image_id: api_image_id)
56
+ end
57
+ end
58
+ ```
59
+
60
+ ```ruby
61
+ # create_deployment.rb
62
+
63
+ deployment = WebApiDeployment.new(env_name: "production", region: "us-east-1", dry_run: false)
64
+ deployment.create("ami-0f71234567890a7f2")
65
+ ```
66
+
67
+ Now let's call the script (making sure our AWS secrets are populated into our environment):
68
+
69
+ ```
70
+ /myapp $> ./create_deployment.rb
71
+ I, [2019-04-26T09:13:58.133753 #12173] INFO -- : Creating production-web-api-network stack...
72
+ D, [2019-04-26T09:14:29.543717 #12173] DEBUG -- : Waiting for production-web-api-network stack to be created... (0m30s elapsed)
73
+ D, [2019-04-26T09:14:59.923172 #12173] DEBUG -- : Waiting for production-web-api-network stack to be created... (1m1s elapsed)
74
+ I, [2019-04-26T09:15:00.306983 #12173] INFO -- : production-web-api-network has been created!
75
+ I, [2019-04-26T09:15:01.133753 #12173] INFO -- : Creating production-web-api-app stack...
76
+ D, [2019-04-26T09:15:31.543717 #12173] DEBUG -- : Waiting for production-web-api-app stack to be created... (0m30s elapsed)
77
+ D, [2019-04-26T09:16:01.923172 #12173] DEBUG -- : Waiting for production-web-api-app stack to be created... (1m1s elapsed)
78
+ I, [2019-04-26T09:16:02.306983 #12173] INFO -- : production-web-api-app has been created!
79
+ ```
80
+
81
+ That's it. Now you have two stacks deployed with minimal fuss. Behind the scenes, the gem found your
82
+ templates, validated them, uploaded them to S3, populated inferred template parameters and capabilities, made
83
+ standardized stack names, managed instance key pairs, waited for stacks to complete, and more.
84
+ Major functionality not shown in this example are the methods for updating and deleting deployments, as well as the mechanism for
85
+ conveying secret values to instances.
86
+
87
+ ## Deployments & Stacks
88
+
89
+ In a deployment supported by this gem, `Stack` objects do the heavy lifting of calling CloudFormation APIs.
90
+ Deployment objects contain a number of `Stack`s and coordinate their use to effect the creation, updating, and
91
+ deletion of applications and infrastructure. For each application or separable infrastructure you'll have a
92
+ deployment class that inherits from `OpenStax::Aws::DeploymentBase`.
93
+
94
+ ### Defining stacks (manually)
95
+
96
+ You can instantiate `OpenStax::Aws::Stack` objects directly. E.g. your deployment class may look like:
97
+
98
+ ```ruby
99
+ class MyDeployment < OpenStax::Aws::DeploymentBase
100
+
101
+ attr_reader :network_stack, :app_stack
102
+
103
+ def initialize(env_name:, name:, region:, dry_run:)
104
+ super(env_name: env_name, name: name, region: region, dry_run: dry_run)
105
+
106
+ @network_stack = OpenStax::Aws::Stack.new(
107
+ name: "#{env_name}-#{name}-network",
108
+ region: region,
109
+ absolute_template_path: File.join(__dir__, "../../templates/network.yml"),
110
+ enable_termination_protection: env_name == "production",
111
+ parameter_defaults: {
112
+ env_name: env_name
113
+ },
114
+ dry_run: dry_run
115
+ )
116
+
117
+ @app_stack = OpenStax::Aws::Stack.new(
118
+ name: "#{env_name}-#{name}-app",
119
+ region: region,
120
+ absolute_template_path: File.join(__dir__, "../../templates/app.yml"),
121
+ enable_termination_protection: env_name == "production",
122
+ parameter_defaults: {
123
+ env_name: env_name,
124
+ network_stack_name: @network_stack.name,
125
+ },
126
+ dry_run: dry_run
127
+ )
128
+ end
129
+
130
+ def create(app_image_id)
131
+ @network_stack.create(wait: true)
132
+ @app_stack.create(params: {image_id: app_image_id})
133
+ end
134
+ end
135
+ ```
136
+
137
+ You create instance variables, make them accessible via `attr_reader` and set a bunch of options.
138
+ A lot of these options end up being duplicated, and when you get into a deployment that has a bunch
139
+ of stacks, this duplication gets a bit heavy.
140
+
141
+ ### Defining stacks (using the DSL)
142
+
143
+ This gem provides an alternative way to declare stacks. The following code is equivalent to the
144
+ manual use of stacks above:
145
+
146
+ ```ruby
147
+ class MyDeployment < OpenStax::Aws::DeploymentBase
148
+
149
+ template_directory __dir__, "../../templates"
150
+
151
+ stack :network
152
+ stack :app
153
+
154
+ def initialize(env_name:, name:, region:, dry_run:)
155
+ super(env_name: env_name, name: name, region: region, dry_run: dry_run)
156
+ end
157
+
158
+ def create(app_image_id)
159
+ network_stack.create(wait: true)
160
+ app_stack.create(params: {image_id: app_image_id})
161
+ end
162
+ end
163
+ ```
164
+
165
+ Here, we've removed a good bit of code by calling the `stack` class method to define our two
166
+ stacks. The `stack` method uses knowledge of the deployment in which it is called in addition
167
+ to some conventions to fill in smart defaults for many of the stack options.
168
+
169
+ * It makes the `:network` stack accessible via `network_stack`.
170
+ * It standardizes the stack name in AWS as `env_name-deployment_name-stack_symbol`, where any underscores
171
+ are replaces with hyphens.
172
+ * It sets the stack region to the the deployment's region.
173
+ * For the `:network` stack, it automatically finds template files named `network.yml` or `network.json`
174
+ in the declared `template_directory`.
175
+ * It enables stack termination protection if the deployment's environment name is the
176
+ production environment name (which itself is configured in the settings).
177
+ * It set's the stack's `dry_run` field to the deployment's `dry_run` value.
178
+ * It detects standard parameters in the template that it knows the values for, e.g. `EnvName`,
179
+ `KeyName` (or `KeyPairName`), and `[Anything]StackName` and fills in those values as the
180
+ stack's parameter defaults.
181
+
182
+ If you don't want to use this automagic setting of values, you can manually define a few of them:
183
+
184
+ ```ruby
185
+ stack :app do
186
+ region { "us-west-2" }
187
+ absolute_template_path { File.join(__dir__, "../../templates/app_variant.yml") }
188
+ parameter_defaults do
189
+ env_name { "april-11-b" }
190
+ end
191
+ end
192
+ ```
193
+
194
+ Blocks passed to the stack definition methods (like `region` above) are executed in the context of the
195
+ containing deployment object.
196
+
197
+ When using the DSL, you can define a stack-local `template_directory` to override the deployment-level
198
+ one, e.g.
199
+
200
+ ```ruby
201
+ stack :app do
202
+ template_directory __dir__, "../somewhere/else"
203
+ end
204
+ ```
205
+
206
+ You can also set a relative template path to override the inferred template filename (can be done
207
+ with or without local modification of the template directory):
208
+
209
+ ```ruby
210
+ stack :app do
211
+ relative_template_path "foo/app_2.yml"
212
+ end
213
+ ```
214
+
215
+ ### Working with stacks
216
+
217
+ Whichever way you choose to define your stack, your next step is to call methods to create, update, and
218
+ delete your stacks in AWS. You do this with the `Stack` methods `create`, `apply_change_set`, and
219
+ `delete`.
220
+
221
+ Each of these methods takes a `wait` boolean (defaults to `false`), which will wait for the
222
+ operation to complete in AWS before returning. You can also separately make a call to wait for stack
223
+ operations to complete, which is useful when you want to simultaneously run operations in several independent stacks
224
+ and then wait for all of them at the same time, e.g.
225
+
226
+ ```ruby
227
+ network_stack.create # these return immediately after starting the creation in AWS
228
+ sqs_stack.create # (meaning all three happen concurrently)
229
+ dynamodb_stack.create
230
+
231
+ network_stack.wait_for_creation # blocks until network_stack created
232
+ sqs_stack.wait_for_creation # blocks until sqs_stack created (may have finished already)
233
+ dynamodb_stack.wait_for_creation
234
+ ```
235
+
236
+ There are three waiter methods on `Stack`: `wait_for_creation`, `wait_for_update`, `wait_for_deletion`.
237
+
238
+ Other things to know about stacks:
239
+
240
+ * You can get stack output values by calling `my_stack.output_value(key: "whatever-the-key-is")`
241
+ * Stack parameter names are typically camel-cased in the template, but in Ruby we write them
242
+ underscored, so e.g. a `EnvName` parameter in the template is referred to in the ruby code as
243
+ `env_name`. You'll also have noticed that the way we pass parameters is simplified so that we just
244
+ have to use Ruby hashes instead of the more verbose `ParameterKey` `ParameterValue` breakdown
245
+ used in the SDK.
246
+
247
+ #### `create`
248
+
249
+ [ Work on this section ]
250
+
251
+ #### `apply_change_set`
252
+
253
+ [ Work on this section ]
254
+
255
+ When updating a stack, call `apply_change_set` with just the parameters you want to change. The gem will build the
256
+ parameters to include in the update using the following algorithm:
257
+
258
+ 1. First, every parameter in the currently-deployed stack that is also in the template being used in
259
+ the update will be included with a value of `:use_previous_value`.
260
+ 2. Next, those parameters in the template being used in the update that are not already in the deployed
261
+ stack will be included with a default value defined in the stack definition (in the `parameter_defaults` block)
262
+ 3. Next, "volatile parameters" are set. Volatile parameters are those that can change outside of stack updates (e.g. autoscaling group desired
263
+ capacities that change due to scaling events). See below for more of a discussion of volatile parameters.
264
+ Volatile parameters will override any existing parameter values.
265
+ 4. Finally, parameters explicitly set in the call to `apply_change_set` will be included. These values
266
+ will override any existing parameter values.
267
+
268
+ Any parameter that ends up with a `nil` value after these series of steps will be removed. Those parameters
269
+ will need to have a default value set in the CloudFormation template file to avoid the call failing within AWS.
270
+
271
+ ##### `parameter_defaults`
272
+
273
+ Use the `parameter_defaults` section of the stack defintion to give default values for stack parameters.
274
+ Entries within the block are of the form `parameter_name value` or `parameter_name { value }`. When the
275
+ latter block form is used, the block is executed in the context of the stack's deployment object when
276
+ the stack is first accessed.
277
+
278
+ ```ruby
279
+ stack :app do
280
+ ...
281
+ parameter_defaults do
282
+ web_server_desired_capacity 2
283
+ some_other_parameter_name { "#{env_name}-blah" }
284
+ end
285
+ ...
286
+ end
287
+ ```
288
+
289
+ Deployment-wide parameter defaults can be defined via a `parameter_defaults(parameter_name)` method on your
290
+ deployment class. This method should return the default value given a parameter name (as it is shown in
291
+ the template), e.g.
292
+
293
+ ```ruby
294
+ class MyDeployment < OpenStax::Aws::DeploymentBase
295
+ ...
296
+ def parameter_default(parameter_name)
297
+ "my-log-bucket" if parameter_name == "LogBucketName"
298
+ end
299
+ ...
300
+ end
301
+ ```
302
+
303
+ The `OpenStax::Aws::DeploymentBase` class provides a `built_in_parameter_default(parameter_name)` method
304
+ for some baseline defaults, e.g. for `"EnvName"` and parameters that are names of other stacks. If you
305
+ want to disable these extra defaults, you can override the method and have it return nil.
306
+
307
+ ```ruby
308
+ class MyDeployment < OpenStax::Aws::DeploymentBase
309
+ ...
310
+ def built_in_parameter_default(parameter_name)
311
+ nil
312
+ end
313
+ ...
314
+ end
315
+ ```
316
+
317
+ ##### `volatile_parameters`
318
+
319
+ By and large, we want to make changes to our stacks using the update stack capabilities offered by AWS.
320
+ However, sometimes there are parameters that change outside of this update process. The classic example
321
+ is autoscaling group desired capacity. When we define an ASG in a template, we define its desired capacity
322
+ (if we don't, it defaults to the minimum allowed capacity). If we give its desired capacity as a static number,
323
+ it'll have that value for each update, meaning that if we create it with a desired capacity of '2' and then
324
+ a scaling event causes the desired capacity to change to 6, an update through CloudFormation will reset the
325
+ desired capacity to 2. Even if the capacity is not a literal number but instead a stack parameter, a stack
326
+ update call that uses the "use previous value" functionality will use the value last used during a stack update
327
+ and not the current value of the desired capacity in the stack. What we want to do is get the stack's current
328
+ desired capacity and use that value in the stack update call so that the update leaves the capacity unchanged.
329
+
330
+ Volatile parameters let us do that. They define the parameters that can change outside of stack updates and
331
+ define a block of code to run just before the update to get that latest value. That value is then used in the
332
+ stack update. The form within the `volatile_parameters` block is `parameter_name { value }` where the `{ value }`
333
+ block is executed in the context of the stack object, just before the call to update the stack.
334
+
335
+ ```ruby
336
+ stack :app do
337
+ ...
338
+ volatile_parameters do
339
+ web_server_desired_capacity { resource("Asg").desired_capacity }
340
+ end
341
+ ...
342
+ end
343
+ ```
344
+
345
+ Here, we're getting the resource in our stack with the `Asg` logical ID (which returns an `Aws::AutoScaling::Group`
346
+ instance) and then getting its desired capacity.
347
+
348
+ #### `delete`
349
+
350
+ No options here besides `wait`.
351
+
352
+ #### `query`
353
+
354
+ A class method `query` is provided on the `Stack` class to find stacks matching certain criteria:
355
+
356
+ * `regex`: only queries whose name matches this regex will be returned (default: `/.*/`)
357
+ * `regions`: only stacks in these regions will be queried (default: US regions)
358
+ * `active`: if `true`, excludes stacks that have been deleted or that failed to create (default: `true`)
359
+ * `reload`: if `true`, forces cached query data to be reloaded (default: `false`)
360
+
361
+ ```ruby
362
+ all_highlights_stacks_in_us = OpenStax::Aws::Stack.query(regex: /.*highlights.*/)
363
+ ```
364
+
365
+ ### Secrets
366
+
367
+ `openstax_aws` uses the AWS Parameter Store as a holding area for application secrets (and here "secrets" includes configuration values that are both secret and not secret). During deployment, secrets are written to the Parameter Store, and then during instance launch they are read from the Parameter Store.
368
+
369
+ ![Secrets sequence diagram - https://www.websequencediagrams.com/?lz=RGVwbG95IHNjcmlwdC0-U3RhY2s6Y3JlYXRlCgAIBQAMCGdldCBzZWNyZXRzIHNwZWNpZmljYXRpb24ADBt1YnN0aXR1dGlvbnMARghQYXJhbWV0ZXIgU3RvcmU6d3JpdGUAUggAawhFQzI6bGF1bmNoCkVDMgAjEgCBAgsK&s=default](assets/secrets_sequence_diagram.png)
370
+
371
+ Secrets are defined with a *specification* and a set of *substitutions*. A specification can currently be defined as a string or file containing YAML, like the following:
372
+
373
+ ```yaml
374
+ secret_key: random(hex,4)
375
+ my_domain: "https://{{ domain }}"
376
+ search_domain:
377
+ endpoint: "{{ search_endpoint }}"
378
+ ```
379
+
380
+ The specification gives (possibly nested) secret names and the values for them, which can be literal values, values that need substitutions populated into them, computed values, or references to other values within the Parameter Store. The specification can be defined inline:
381
+
382
+ ```ruby
383
+ OpenStax::Aws::SecretsSpecification.from_content(
384
+ format: :yml,
385
+ content: <<~CONTENT
386
+ graylog_url: ssm(graylog_url)
387
+ CONTENT
388
+ )
389
+ ```
390
+
391
+ or via a reference to a file on GitHub at a SHA:
392
+
393
+ ```ruby
394
+ OpenStax::Aws::SecretsSpecification.from_git(
395
+ org_slash_repo: "openstax/open-search",
396
+ sha: some_sha_here,
397
+ path: 'config/secrets.yml.example',
398
+ format: :yml,
399
+ top_key: :production
400
+ )
401
+ ```
402
+
403
+ Why not just write the secrets to the instances directly? In a world where we are deploying Amazon Machine Images or Docker containers across environments and clouds, we don't want the secrets to live in these files because (1) we don't want secret values written in plaintext in a the image file and (2) when launched those images/containers will need different secrets based on where they are launched. This is why instead we work to get the secrets into an accessible location and then have the running app pull them when it needs them.
404
+
405
+ #### How secrets get written to the Parameter Store
406
+
407
+ When secrets are written to the Parameter Store, their nested structure is combined with a caller-specified namespace prefix to form the Parameter Store key. E.g. for the following specification:
408
+
409
+ ```ruby
410
+ a:
411
+ b:
412
+ c: "my value"
413
+ ```
414
+
415
+ And a namespace of `qa/search/api`, the following is written to the Parameter Store:
416
+
417
+ ```
418
+ Key: /qa/search/api/a/b/c
419
+ Value: my value
420
+ ```
421
+
422
+ When using the secrets DSL, the namespace is `env_name/deployment_name/stack_name`.
423
+
424
+ #### Kinds of secrets
425
+
426
+ In the specification, secrets can have literal values, e.g. `"some static string"`. But the real power of secrets is that their values can be substituted, generated, or pulled in via reference:
427
+
428
+ ##### Literal secrets
429
+
430
+ Specification:
431
+
432
+ ```ruby
433
+ some_boring_secret: "this string never changes"
434
+ ```
435
+
436
+ Result in Parameter Store:
437
+
438
+ ```
439
+ Key: /env_name/more_namespace/some_boring_secret
440
+ Value: this string never changes
441
+ ```
442
+
443
+ ##### Substituted secrets
444
+
445
+ Specification:
446
+
447
+ ```ruby
448
+ a_substitution_secret: "this string's ending changes {{ ending }}"
449
+ ```
450
+
451
+ Substitutions:
452
+
453
+ ```ruby
454
+ ending: "oh yeah it does"
455
+ ```
456
+
457
+ Result in Parameter Store:
458
+
459
+ ```
460
+ Key: /env_name/more_namespace/a_substitution_secret
461
+ Value: this string's ending changes oh yeah it does
462
+ ```
463
+
464
+ ##### Computed secrets
465
+
466
+ Computed secrets are good for generating random strings.
467
+
468
+ Specification:
469
+
470
+ ```ruby
471
+ my_secret_key: random(hex, 8)
472
+ ```
473
+
474
+ Result in Parameter Store:
475
+
476
+ ```
477
+ Key: /env_name/more_namespace/my_secret_key
478
+ Value: 019af8dc
479
+ ```
480
+
481
+ Instead of `random(hex, number_of_hex_characters])` you can use `uuid` to get a UUID or `base64` to get a URL-safe base 64 string or `rsa(size_of_key)` to generate a private SSL RSA key. Note that generated secrets are only updated during a stack update if their specification changes (that way things like randomly generated secret keys don't change on each deployment unless how the value is computed changes).
482
+
483
+ Also note that if an array of secrets in the specification contains a generated secret, the overall `StringList` secret written to the parameter store is not marked as generated and so _would_ be changed in an update; the lesson of which is that this library only partially handles generative secrets in arrays.
484
+
485
+ ##### Referential secrets
486
+
487
+ Referential secrets let you say that a secret should take the value of another parameter in the parameter store. They
488
+ are good for defining static secret keys in the parameter store that are shared across many deployments, e.g.
489
+ some fixed OAuth keys or a common logging endpoint.
490
+
491
+ Assume that the parameter store has the following value:
492
+
493
+ ```
494
+ Key: /external/graylog/secret
495
+ Value: cf9bb194b53d76a557c8
496
+ ```
497
+
498
+ Then in your specification:
499
+
500
+ ```ruby
501
+ my_graylog_secret: ssm(graylog_secret)
502
+ ```
503
+
504
+ Substitutions:
505
+
506
+ ```ruby
507
+ graylog_secret: "/external/graylog/secret"
508
+ ```
509
+
510
+ Result in Parameter Store:
511
+
512
+ ```
513
+ Key: /env_name/more_namespace/my_graylog_secret
514
+ Value: cf9bb194b53d76a557c8
515
+ ```
516
+
517
+ We lookup the value inside `ssm(...)` using substitutions so that different environments can point to different
518
+ values in the Parameter Store, which is useful if you have say two secret values, one for development and one
519
+ for production deployments.
520
+
521
+ Note that you can also use referential secrets without substitutions:
522
+
523
+ ```yaml
524
+ my_graylog_secret: ssm(/external/graylog/secret)
525
+ ```
526
+
527
+ #### Encrypted Secrets
528
+
529
+ Most of the secrets we store are really just configuration values, and these we store in the Parameter Store with type "String". However, the Parameter Store also lets us store parameters as "SecureString"s, which means that AWS encrypts them for us. The secrets handling code can deal with these kinds of parameters.
530
+
531
+ When we use referential secrets the are SecureString encrypted, the secrets that are copied to an environment's section of the Parameter Store are also SecureString encrypted.
532
+
533
+ #### Secrets DSL
534
+
535
+ While you can instantiate secrets and specifications objects directly, it is easiest to use the DSL. The DSL lets you define secrets on a per-stack basis:
536
+
537
+ ```ruby
538
+ stack :api do
539
+ ...
540
+ secrets do |parameters|
541
+ namespace "my-app/api"
542
+ specification do
543
+ org_slash_repo { "my-org/my-repo" }
544
+ path { "config/secrets.yml.example" }
545
+ sha { parameters.sha }
546
+ format { :yml }
547
+ top_key { :production } # optional
548
+ preparser { :erb } # optional, use if your YAML file has embedded Ruby
549
+ end
550
+ substitutions do
551
+ domain { domain }
552
+ env_name { env_name }
553
+ elasticsearch_endpoint { elasticsearch_stack.output_value(key: "endpoint")}
554
+ end
555
+ end
556
+ ...
557
+ end
558
+ ```
559
+
560
+ Specification and substitution blocks are executed in the context of the containing deployment. If you have the `secrets` block take a `parameters` argument, that will give you access to the stack's parameters, which is useful for getting the SHA being deployed for the stack (so you can get the secrets specification to match the deployment).
561
+
562
+ You can call the `secrets` DSL multiple times within the `stack` declaration. You may also call `specification` multiple times within the `secrets` call. Later declarations can override earlier ones.
563
+
564
+ When stacks are updated, only the secrets that change are updated.
565
+
566
+ Sometimes multiple stacks share the same substitutions. Instead of repeating those substitutions, you can define them once in your deployment class:
567
+
568
+ ```ruby
569
+ class MyDeployment < OpenStax::Aws::DeploymentBase
570
+ ...
571
+ secrets_substititutions do
572
+ domain { my_domain }
573
+ end
574
+ ...
575
+
576
+ def my_domain
577
+ "#{env_name}.example.com"
578
+ end
579
+ end
580
+ ```
581
+
582
+ Substitutions defined in this way will be overridden by any substitutions defined directly in the stack.
583
+
584
+ When you create, update, or delete a stack that uses the secrets DSL, the secrets are automatically created, updated, or deleted before the stack resources are modified.
585
+
586
+ Sometimes you might need to create secrets within a deployment but outside of any particular stack (e.g. to define a database password to be used by an RDS stack and API server stack). You can use the secrets DSL at the deployment level just like you do at the stack level:
587
+
588
+ ```ruby
589
+ class MyDeployment < OpenStax::Aws::DeploymentBase
590
+ secrets :common do
591
+ # same stuff here as for stack-level secrets
592
+ end
593
+ end
594
+ ```
595
+
596
+ These deployment-level secrets can then be accessed via `{ID}_secrets`, e.g. `common_secrets` in the example above.
597
+
598
+ You can define multiple deployment-level secrets, but they must all have unique names compared to each other and to defined stacks, e.g. you cannot do:
599
+
600
+ ```ruby
601
+ secrets :foo
602
+ stack :foo
603
+ ```
604
+
605
+ While stack-level secrets are created, updated, and deleted for you when you perform those actions on the stacks that contain them, deployment-level secrets must created, updated, and deleted explicitly. E.g. you may have secrets that you create first thing inside
606
+ your deployment's `create` method:
607
+
608
+ ```ruby
609
+ class MyDeployment < OpenStax::Aws::DeploymentBase
610
+ secrets :common do
611
+ specification do
612
+ content do
613
+ { database_password: 'random(hex,15)' }
614
+ end
615
+ end
616
+ end
617
+
618
+ def create
619
+ common_secrets(for_create_or_update: true).create
620
+
621
+ rds_stack.create(params: {master_password: common_secrets.get(:database_password)})
622
+
623
+ # ...
624
+ end
625
+ end
626
+ ```
627
+
628
+ #### Loading secrets from the Parameter Store
629
+
630
+ When your instance or container launches, you'll want it to access its secrets in the Parameter Store so it can use them however is needed
631
+ in your application.
632
+
633
+ The key structure of each Parameter Store value helps you in two ways:
634
+
635
+ (1) You can limit your application's ability to read values in the parameter store by the namespace of each value, e.g. if you want
636
+ to limit the QA search deployment to access only the QA search secrets, you could use a CloudFormation template IAM policy such as:
637
+
638
+ ```
639
+ - PolicyName: !Sub '${EnvName}-read-parameters'
640
+ PolicyDocument:
641
+ Version: '2012-10-17'
642
+ Statement:
643
+ - Effect: Allow
644
+ Action:
645
+ - ssm:DescribeParameters
646
+ Resource: '*'
647
+ - Effect: Allow
648
+ Action:
649
+ - ssm:GetParametersByPath
650
+ - ssm:GetParameters
651
+ Resource: !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/${EnvName}/search'
652
+ ```
653
+
654
+ (2) That the nesting of the keys from the secrets specification is preserved in the Parameter Store key lets you recover it when accessing your secrets. E.g. here's how one of our apps uses the AWS Ruby SDK to take the secrets in the Parameter Store and writes them to the app's nested `config/secrets.yml` file:
655
+
656
+ ```ruby
657
+ client = Aws::SSM::Client.new(region: region)
658
+ client.get_parameters_by_path({path: "/#{env_name}/#{namespace}/",
659
+ recursive: true,
660
+ with_decryption: true}).each do |response|
661
+ response.parameters.each do |parameter|
662
+ # break out the flattened keys and ignore the env name and namespace
663
+ keys = parameter.name.split('/').reject(&:blank?)[2..-1]
664
+ deep_populate(secrets, keys, parameter.value)
665
+ end
666
+ end
667
+
668
+ File.open(File.expand_path("config/secrets.yml"), "w") do |file|
669
+ # write the secrets hash as yaml, getting rid of the "---\n" at the front
670
+ file.write({'production' => secrets}.to_yaml[4..-1])
671
+ end
672
+
673
+ def deep_populate(hash, keys, value)
674
+ if keys.length == 1
675
+ hash[keys[0]] = value
676
+ else
677
+ hash[keys[0]] ||= {}
678
+ deep_populate(hash[keys[0]], keys[1..-1], value)
679
+ end
680
+ end
681
+ ```
682
+
683
+ #### Forcing servers to cycle when their secrets change
684
+
685
+ When an app's secrets change, we want their new values to be used by the app. But our apps typically get their secrets when they launch, and launches happen when CloudFormation sees a change in the template that requires an update (e.g. a new AMI is specified). When we make changes to the values in the Parameter Store, CloudFormation doesn't see them and therefore does not trigger an update of our servers.
686
+
687
+ This gem provides a mechanism for changes in secrets to trigger an update of the servers that use them. When a stack update is called and the secrets defined within the stack (via the DSL) change, the gem will set a random value in a user-defined stack parameter. If that stack parameter is then included in an ASG's launch configuration, CloudFormation will detect a change in that launch configuration and trigger a server update.
688
+
689
+ Here's a snippet of a template that will accept and use this special parameter:
690
+
691
+ ```yaml
692
+ AWSTemplateFormatVersion: '2010-09-09'
693
+
694
+ Parameters:
695
+
696
+ ...
697
+
698
+ CycleIfDifferent:
699
+ Description: A special parameter that will be set to a random value when this stack's secrets change
700
+ Type: String
701
+ Default: ''
702
+
703
+ Resources:
704
+
705
+ ...
706
+
707
+ Lc:
708
+ Type: AWS::AutoScaling::LaunchConfiguration
709
+ Properties:
710
+ ImageId: !Ref 'WebServerImageId'
711
+ InstanceType: t2.micro
712
+ UserData:
713
+ Fn::Base64:
714
+ !Sub |
715
+ #!/bin/bash -x
716
+
717
+ # ${CycleIfDifferent} <-- this gets randomized when secrets change, which forces server updates
718
+ ...
719
+
720
+ Asg:
721
+ Type: AWS::AutoScaling::AutoScalingGroup
722
+ Properties:
723
+ LaunchConfigurationName: !Ref 'Lc'
724
+ ...
725
+ ```
726
+
727
+ `CycleIfDifferent` is the default name of this special stack parameter. This default can be overridden in the gem configuration via
728
+
729
+ ```ruby
730
+ OpenStax::Aws.configuration.default_cycle_if_different_parameter = "MyPreferredParameter"
731
+ ```
732
+
733
+ or it can be set within the `stack` declaration:
734
+
735
+ ```ruby
736
+ stack :api do
737
+ ...
738
+ cycle_if_different_parameter "MyPreferredParameter"
739
+ ..
740
+ ```
741
+
742
+ ### Tags
743
+
744
+ You can specify stack tags with:
745
+
746
+ ```ruby
747
+ stack :network do
748
+ ...
749
+ tag :Foo, "bar"
750
+ ...
751
+ end
752
+ ```
753
+
754
+ You can specify tags in all of a deployment's stacks by setting the tags at the deployment level:
755
+
756
+ ```ruby
757
+ class MyDeployment < OpenStax::Aws::DeploymentBase
758
+ tag :Application, "my_app"
759
+ tag :Owner, "Jimmy"
760
+ tag(:Environment) { env_name }
761
+ ...
762
+
763
+ def env_name
764
+ @env_name
765
+ end
766
+ ```
767
+
768
+ Note the block form of `tag` sets the tag value by evaluating the block in the context of the deployment instance,
769
+ in this case by calling `env_name` on the deployment instance.
770
+
771
+ Tags set at the stack level will override those set at the deployment level.
772
+
773
+ You can enforce that stacks always have certain tags by setting `OpenStax::Aws.configuration.required_stack_tags`. By default, there
774
+ are a few required tags; if you want to disable this, set this variable to `[]`.
775
+
776
+ ### Dry runs
777
+
778
+ You'll have noticed above that deployment and `Stack` objects are instantiated with a `dry_run` parameter
779
+ that defaults to `true`. When `dry_run` is true, stacks are not created, updated, or deleted, but the
780
+ code is exercised and log messages are generated. Note that during dry run updates, CloudFormation change
781
+ sets are temporarily created on AWS but they are not executed.
782
+
783
+ ### Configuration
784
+
785
+ You can configure gem behavior with:
786
+
787
+ ```ruby
788
+ OpenStax::Aws.configure do |config|
789
+ # The bucket where you want to upload templates
790
+ config.cfn_template_bucket_name = "some-bucket-name"
791
+ # The top-level folder(s) where templates are stored in the template bucket.
792
+ # (default: "cfn_templates")
793
+ config.cfn_template_bucket_folder = "cfn_templates"
794
+ # A logger object, e.g. `Logger.new(STDOUT)` (the default); see below for more.
795
+ config.logger = Logger.new(STDOUT)
796
+ # The number of seconds the gem waits between polling for the completion of stack creation,
797
+ # updates, deletes. (default: 30)
798
+ config.stack_waiter_delay = 30
799
+ # The number of maximum attempts the gem makes to check for the completion of stack creation,
800
+ # updates, deletes. (default: 180)
801
+ config.stack_waiter_max_attempts = 180
802
+ # If true, the gem will parse your template and autoset the required capabilities
803
+ # (default: true)
804
+ config.infer_stack_capabilities = true
805
+ # If true, the gem will set default values for parameters that it knows about (e.g.
806
+ # EnvName, default: true)
807
+ config.infer_parameter_defaults = true
808
+ # The environment name you use for production, e.g. "prod" or "production" (the default).
809
+ config.production_env_name = "production"
810
+ end
811
+ ```
812
+
813
+ We use the logger configuration below:
814
+
815
+ ```ruby
816
+ config.logger = Logger.new(STDERR)
817
+ config.logger.formatter = proc do |severity, datetime, progname, msg|
818
+ date_format = datetime.strftime("%Y-%m-%d %H:%M:%S.%3N")
819
+ if severity == "INFO" or severity == "WARN"
820
+ "[#{date_format}] #{severity} | #{msg}\n"
821
+ else
822
+ "[#{date_format}] #{severity} | #{msg}\n"
823
+ end
824
+ end
825
+ ```
826
+
827
+ ### Templates
828
+
829
+ While you won't usually use it directly, there is an `OpenStax::Aws::Template` object that you can
830
+ use to get values back from your template. A stack's template is available via `my_stack.template`.
831
+ Templates are uploaded to S3 when its `Stack` needs it to be there. Before it is uploaded, it is
832
+ validated and errors are raised to you.
833
+
834
+ ### AutoScalingInstance
835
+
836
+ An `OpenStax::Aws::AutoScalingInstance` class is provided that wraps and extends the `Aws::AutoScaling::Instance`
837
+ class. Methods that it doesn't implement are delegated to the AWS SDK class.
838
+
839
+ It has a class method, `me`, that returns an `AutoScalingInstance` for a instance running within AWS. This
840
+ method requires the `autoscaling:DescribeAutoScalingInstances` permission on the instance calling this method
841
+ or attached to the credentials being used to call this method.
842
+
843
+ It has ASG-lifecycle-hook-aware methods, e.g. its implementation of `terminate` takes the same arguments as the
844
+ SDK class (`should_decrement_desired_capacity`) but also takes an `continue_hook_name` parameter with a
845
+ termination lifecycle hook name. If that parameter is provided, the `terminate` call will wait for the
846
+ lifecycle state `Terminating::Wait` to be reached before calling the SDK client to continue on from that
847
+ state to complete the termination.
848
+
849
+ It also has an `unless_waiting_for_termination` method that checks to see if the instance is in the
850
+ `Terminating::Wait` state. If it isn't, the block of code passed to the method is executed. If it is
851
+ in that state, the block of code isn't executed and instead the termination is completed.
852
+
853
+ ```ruby
854
+ OpenStax::Aws::AutoScalingInstance.me.unless_waiting_for_termination do
855
+ # some code that you don't want interrupted by a termination
856
+ end
857
+ ```
858
+
859
+ The `unless_waiting_for_termination` method checks for the terminating wait state just before and
860
+ just after the block is yielded to.
861
+
862
+ ### S3TextFile
863
+
864
+ `S3TextFile` is a helper class for writing, reading, and deleting text files on S3.
865
+
866
+ ```ruby
867
+ s3_text_file = OpenStax::Aws::S3TextFile.new(bucket_name: "my-bucket", bucket_region: "us-east-2", key: "some/path/foo.txt")
868
+ s3_text_file.write(string_contents: "Howdy")
869
+ s3_text_file.read #=> "Howdy"
870
+ s3_text_file.delete
871
+ ```
872
+
873
+ Raises `Aws::S3::Errors::NotFound` when trying to `read` a file that does not exist. Calling `delete` on a file that doesn't
874
+ exist does not raise an error.
875
+
876
+ You can also set the content type and cache control headers, e.g.:
877
+
878
+ ```ruby
879
+ s3_text_file.write(string_contents: "...", content_type: "application/json", cache_control: "max-age=0")
880
+ ```
881
+
882
+ ### SAM
883
+
884
+ You can include [AWS SAM](https://aws.amazon.com/serverless/sam/) (Serverless Application Model) stacks in your deployments, just like you would any other stack:
885
+
886
+ ```ruby
887
+ stack :sam # use whatever name you want
888
+ ```
889
+
890
+ SAM stacks will be automatically detected by finding the "Transform: AWS::Serverless" text in its template.
891
+
892
+ When you have a SAM stack, you need to declare a build directory in your deployment, e.g.:
893
+
894
+ ```ruby
895
+ class Deployment < OpenStax::Aws::DeploymentBase
896
+ sam_build_directory __dir__, '../.aws-sam'
897
+ ```
898
+
899
+ You should `gitignore` this directory. Because there isn't an API or SDK for AWS SAM, you'll need the SAM cli installed where this code is run.
900
+
901
+ Within the deployment class, the stack variable provided for you, `sam_stack` in the example above, will be of type `OpenStax::Aws::SamStack` instead of the normal `OpenStax::Aws::Stack`. `SamStack` inherits from `Stack` and adds two methods: `build` and `deploy`, which make the SAM CLI calls to build and deploy the stack. `deploy` is a method that creates the stack if it doesn't exist or updates it if it does.
902
+
903
+ Where it is common practice for us to have `create`, `update`, and `delete` scripts that call these methods on our deployments, with a SAM stack in the mix, you'll probably also want a `build` script to call a `build` method on your deployment class which turns around and calls `build` on your SAM stack. Then you can choose to have `create` and `update` or a `deploy` method on your deployment class, which calls `deploy` on your SAM stack and normal `create` and `update` methods on your non-SAM stacks.
904
+
905
+ ## AWS Development Environment
906
+
907
+ Some methods, e.g. those that retrieve instance metadata, are intended to be run on an actual AWS
908
+ instance. This means that to test them we must be running the tests on an AWS instance. To this
909
+ end, this gem provides a bash script that creates a stack with an autoscaling group containing one
910
+ instance.
911
+
912
+ ```
913
+ $> bin/create_development_environment
914
+ ```
915
+
916
+ You can find the instance's IP address in the AWS console, and then SSH into it. Once there you
917
+ can install ruby, etc, to get the development environment set up. There is a script, `install.sh`
918
+ that has the commands you can use to install ruby et al.
919
+
920
+ Then you can checkout this gem, do your development through SSH, record specs, etc, and commit
921
+ your changes.
922
+
923
+ Don't forget to delete your stack (from the console) when you are finished!
924
+
925
+ ## README Todos
926
+
927
+ 1. Discuss use of multiple secrets objects