openstax_aws 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +11 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +120 -0
- data/LICENSE.txt +1 -0
- data/README.md +927 -0
- data/Rakefile +6 -0
- data/TODO.md +1 -0
- data/assets/secrets_sequence_diagram.png +0 -0
- data/bin/console +14 -0
- data/bin/create_development_environment +26 -0
- data/bin/get_latest_ubuntu_ami +31 -0
- data/bin/setup +8 -0
- data/bin/templates/aws_ruby_development.yml +221 -0
- data/examples/deployment.rb +90 -0
- data/ideas.md +15 -0
- data/lib/openstax/aws/auto_scaling_group.rb +28 -0
- data/lib/openstax/aws/auto_scaling_instance.rb +96 -0
- data/lib/openstax/aws/build_image_command_1.rb +53 -0
- data/lib/openstax/aws/change_set.rb +100 -0
- data/lib/openstax/aws/deployment_base.rb +372 -0
- data/lib/openstax/aws/distribution.rb +56 -0
- data/lib/openstax/aws/ec2_instance_data.rb +18 -0
- data/lib/openstax/aws/extensions.rb +19 -0
- data/lib/openstax/aws/git_helper.rb +18 -0
- data/lib/openstax/aws/image.rb +34 -0
- data/lib/openstax/aws/msk_cluster.rb +19 -0
- data/lib/openstax/aws/packer_1_2_5.rb +63 -0
- data/lib/openstax/aws/packer_1_4_1.rb +72 -0
- data/lib/openstax/aws/packer_factory.rb +25 -0
- data/lib/openstax/aws/rds_instance.rb +25 -0
- data/lib/openstax/aws/s3_text_file.rb +50 -0
- data/lib/openstax/aws/sam_stack.rb +85 -0
- data/lib/openstax/aws/secrets.rb +302 -0
- data/lib/openstax/aws/secrets_factory.rb +126 -0
- data/lib/openstax/aws/secrets_set.rb +21 -0
- data/lib/openstax/aws/secrets_specification.rb +68 -0
- data/lib/openstax/aws/stack.rb +465 -0
- data/lib/openstax/aws/stack_event.rb +28 -0
- data/lib/openstax/aws/stack_factory.rb +153 -0
- data/lib/openstax/aws/stack_parameters.rb +19 -0
- data/lib/openstax/aws/stack_status.rb +125 -0
- data/lib/openstax/aws/system.rb +21 -0
- data/lib/openstax/aws/tag.rb +31 -0
- data/lib/openstax/aws/template.rb +129 -0
- data/lib/openstax/aws/version.rb +5 -0
- data/lib/openstax/aws/wait_message.rb +20 -0
- data/lib/openstax_aws.rb +154 -0
- data/openstax_aws.gemspec +58 -0
- metadata +350 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/CHANGELOG.md
ADDED
@@ -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
data/Gemfile.lock
ADDED
@@ -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
|
data/LICENSE.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
MIT
|
data/README.md
ADDED
@@ -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
|