Skip to content

Commit 7246dfa

Browse files
author
Pavel Sinkevych
committed
Manage CloudFromation Stack tags
1 parent 065f2a4 commit 7246dfa

6 files changed

Lines changed: 138 additions & 35 deletions

File tree

README.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ Once running the operator should print some output but shouldn't actually do any
4545

4646
# Demo
4747

48+
## Create stack
49+
4850
Currently you don't have any stacks.
4951

5052
```console
@@ -104,6 +106,8 @@ status:
104106

105107
Voilà, you just created a CloudFormation stack by only talking to Kubernetes.
106108

109+
## Update stack
110+
107111
You can also update your stack: Let's change the `VersioningConfiguration` from `Suspended` to `Enabled`:
108112

109113
```yaml
@@ -135,6 +139,39 @@ Wait until the operator discovered and executed the change, then look at your AW
135139

136140
![Update stack](docs/img/stack-update.png)
137141

142+
## Tags
143+
144+
You may want to assign tags to your CloudFormation stacks. The tags added to a CloudFormation stack will be propagated to the managed resources. This feature may be useful in multiple cases, for example, to distinguish resources at billing report. Current operator provides two ways to assign tags:
145+
- `global-tags` command line argument or `GLOBAL_TAGS` environment variable which allows setting global tags for all resources managed by the operator. This option accepts JSON format where every key is a tag name and value is a tag value. For example '{"foo": "fooValue", "bar": "barValue"}'
146+
- `tags` parameter at kubernetes resource spec:
147+
```yaml
148+
apiVersion: cloudformation.linki.space/v1alpha1
149+
kind: Stack
150+
metadata:
151+
name: my-bucket
152+
spec:
153+
tags:
154+
foo: dataFromStack
155+
template: |
156+
---
157+
AWSTemplateFormatVersion: '2010-09-09'
158+
159+
Resources:
160+
S3Bucket:
161+
Type: AWS::S3::Bucket
162+
Properties:
163+
VersioningConfiguration:
164+
Status: Enabled
165+
```
166+
167+
Resource-specific tags have precedence over the global tags. Thus if a tag is defined at command-line arguments and for a `Stack` resource, the value from the `Stack` resource will be used.
168+
169+
If we run the operation and a `Stack` resource with the described above examples, we'll see such picture:
170+
171+
![Stack tags](docs/img/stack-tags.png)
172+
173+
## Parameters
174+
138175
However, often you'll want to extract dynamic values out of your CloudFormation stack template into so called `Parameters` so that your template itself doesn't change that often and, well, is really a *template*.
139176

140177
Let's extract the `VersioningConfiguration` into a parameter:
@@ -175,6 +212,8 @@ Since we changed the template a little this will update your CloudFormation stac
175212

176213
Any CloudFormation parameters defined in the CloudFormation template can be specified in the `Stack` resource's `spec.parameters` section. It's a simple key/value map.
177214

215+
## Outputs
216+
178217
Furthermore, CloudFormation supports so called `Outputs`. These can be used for dynamic values that are only known after a stack has been created.
179218
In our example, we don't define a particular S3 bucket name but instead let AWS generate one for us.
180219

@@ -233,6 +272,8 @@ status:
233272

234273
In the template we defined an `Output` called `BucketName` that should contain the name of our bucket after stack creation. Looking up the corresponding value under `.status.outputs[BucketName]` reveals that our bucket was named `my-bucket-s3bucket-tarusnslfnsj`.
235274

275+
## Delete stack
276+
236277
The operator captures the whole lifecycle of a CloudFormation stack. So if you delete the resource from Kubernetes, the operator will teardown the CloudFormation stack as well. Let's do that now:
237278

238279
```console
@@ -244,6 +285,16 @@ Check your CloudFormation console once more and validate that your stack as well
244285

245286
![Delete stack](docs/img/stack-delete.png)
246287

288+
# Command-line arguments
289+
290+
Argument | Environment variable | Default value | Description
291+
---------|----------------------|---------------|------------
292+
debug | DEBUG | | Enable debug logging.
293+
dry-run | DRY_RUN | | If true, don't actually do anything.
294+
global-tags | GLOBAL_TAGS | {} | Global tags which should be applied for all stacks. Current parameter accepts JSON format where every key-value pair defines a tag. Key is a tag name and value is a tag value.
295+
namespace | WATCH_NAMESPACE | default | The Kubernetes namespace to watch
296+
region | AWS_REGION | | The AWS region to use
297+
247298
# Cleanup
248299

249300
Clean up the resources:

cmd/cloudformation-operator/main.go

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"runtime"
67

78
"github.com/alecthomas/kingpin"
@@ -17,16 +18,25 @@ import (
1718
)
1819

1920
var (
20-
namespace string
21-
region string
22-
dryRun bool
23-
debug bool
24-
version = "0.2.0+git"
21+
namespace string
22+
region string
23+
globalTags string
24+
dryRun bool
25+
debug bool
26+
version = "0.2.0+git"
2527
)
2628

29+
type Tags map[string]string
30+
2731
func init() {
2832
kingpin.Flag("namespace", "The Kubernetes namespace to watch").Default("default").Envar("WATCH_NAMESPACE").StringVar(&namespace)
2933
kingpin.Flag("region", "The AWS region to use").Envar("AWS_REGION").StringVar(&region)
34+
kingpin.Flag(
35+
"global-tags",
36+
"Global tags which should be applied for all stacks." +
37+
" Current parameter accepts JSON format where every key-value pair defines a tag." +
38+
" Key is a tag name and value is a tag value.",
39+
).Default("{}").Envar("GLOBAL_TAGS").StringVar(&globalTags)
3040
kingpin.Flag("dry-run", "If true, don't actually do anything.").Envar("DRY_RUN").BoolVar(&dryRun)
3141
kingpin.Flag("debug", "Enable debug logging.").Envar("DEBUG").BoolVar(&debug)
3242
}
@@ -38,6 +48,15 @@ func printVersion() {
3848
logrus.Infof("cloudformation-operator Version: %v", version)
3949
}
4050

51+
func parseTags() map[string]string {
52+
var globalTagsParsed map[string]string
53+
err := json.Unmarshal([]byte(globalTags), &globalTagsParsed)
54+
if err != nil {
55+
logrus.Error("Failed to parse global tags: ", err)
56+
}
57+
return globalTagsParsed
58+
}
59+
4160
func main() {
4261
kingpin.Version(version)
4362
kingpin.Parse()
@@ -57,6 +76,6 @@ func main() {
5776
})
5877

5978
sdk.Watch("cloudformation.linki.space/v1alpha1", "Stack", namespace, 0)
60-
sdk.Handle(stub.NewHandler(client, dryRun))
79+
sdk.Handle(stub.NewHandler(client, parseTags(), dryRun))
6180
sdk.Run(context.TODO())
6281
}

docs/img/stack-tags.png

21.6 KB
Loading

examples/cfs-my-bucket-tags.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: cloudformation.linki.space/v1alpha1
2+
kind: Stack
3+
metadata:
4+
name: my-bucket
5+
spec:
6+
tags:
7+
foo: dataFromStack
8+
template: |
9+
---
10+
AWSTemplateFormatVersion: '2010-09-09'
11+
12+
Resources:
13+
S3Bucket:
14+
Type: AWS::S3::Bucket
15+
Properties:
16+
VersioningConfiguration:
17+
Status: Suspended

pkg/apis/cloudformation/v1alpha1/types.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ type Stack struct {
2222
}
2323

2424
type StackSpec struct {
25-
Template string `json:"template"`
2625
Parameters map[string]string `json:"parameters"`
26+
Tags map[string]string `json:"tags"`
27+
Template string `json:"template"`
2728
}
2829
type StackStatus struct {
2930
StackID string `json:"stackID"`

pkg/stub/handler.go

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,13 @@ var (
2929
)
3030

3131
type Handler struct {
32-
client cloudformationiface.CloudFormationAPI
33-
dryRun bool
32+
client cloudformationiface.CloudFormationAPI
33+
globalTags map[string]string
34+
dryRun bool
3435
}
3536

36-
func NewHandler(client cloudformationiface.CloudFormationAPI, dryRun bool) handler.Handler {
37-
return &Handler{client: client, dryRun: dryRun}
37+
func NewHandler(client cloudformationiface.CloudFormationAPI, globalTags map[string]string, dryRun bool) handler.Handler {
38+
return &Handler{client: client, globalTags: globalTags, dryRun: dryRun}
3839
}
3940

4041
func (h *Handler) Handle(ctx types.Context, event types.Event) error {
@@ -81,25 +82,13 @@ func (h *Handler) createStack(stack *v1alpha1.Stack) error {
8182
return nil
8283
}
8384

84-
params := []*cloudformation.Parameter{}
85-
for k, v := range stack.Spec.Parameters {
86-
params = append(params, &cloudformation.Parameter{
87-
ParameterKey: aws.String(k),
88-
ParameterValue: aws.String(v),
89-
})
90-
}
91-
9285
input := &cloudformation.CreateStackInput{
9386
StackName: aws.String(stack.Name),
9487
TemplateBody: aws.String(stack.Spec.Template),
95-
Parameters: params,
96-
Tags: []*cloudformation.Tag{
97-
{
98-
Key: aws.String(ownerTagKey),
99-
Value: aws.String(ownerTagValue),
100-
},
101-
},
88+
Parameters: h.processStackParams(stack),
89+
Tags: h.processStackTags(stack),
10290
}
91+
10392
if _, err := h.client.CreateStack(input); err != nil {
10493
return err
10594
}
@@ -119,18 +108,11 @@ func (h *Handler) updateStack(stack *v1alpha1.Stack) error {
119108
return nil
120109
}
121110

122-
params := []*cloudformation.Parameter{}
123-
for k, v := range stack.Spec.Parameters {
124-
params = append(params, &cloudformation.Parameter{
125-
ParameterKey: aws.String(k),
126-
ParameterValue: aws.String(v),
127-
})
128-
}
129-
130111
input := &cloudformation.UpdateStackInput{
131112
StackName: aws.String(stack.Name),
132113
TemplateBody: aws.String(stack.Spec.Template),
133-
Parameters: params,
114+
Parameters: h.processStackParams(stack),
115+
Tags: h.processStackTags(stack),
134116
}
135117

136118
if _, err := h.client.UpdateStack(input); err != nil {
@@ -184,6 +166,39 @@ func (h *Handler) getStack(stack *v1alpha1.Stack) (*cloudformation.Stack, error)
184166
return resp.Stacks[0], nil
185167
}
186168

169+
func (h *Handler) processStackParams(stack *v1alpha1.Stack) ([]*cloudformation.Parameter) {
170+
params := []*cloudformation.Parameter{}
171+
for k, v := range stack.Spec.Parameters {
172+
params = append(params, &cloudformation.Parameter{
173+
ParameterKey: aws.String(k),
174+
ParameterValue: aws.String(v),
175+
})
176+
}
177+
return params
178+
}
179+
180+
func (h *Handler) processStackTags(stack *v1alpha1.Stack) ([]*cloudformation.Tag) {
181+
tags := []*cloudformation.Tag{
182+
{
183+
Key: aws.String(ownerTagKey),
184+
Value: aws.String(ownerTagValue),
185+
},
186+
}
187+
for k, v := range h.globalTags {
188+
tags = append(tags, &cloudformation.Tag{
189+
Key: aws.String(k),
190+
Value: aws.String(v),
191+
})
192+
}
193+
for k, v := range stack.Spec.Tags {
194+
tags = append(tags, &cloudformation.Tag{
195+
Key: aws.String(k),
196+
Value: aws.String(v),
197+
})
198+
}
199+
return tags
200+
}
201+
187202
func (h *Handler) stackExists(stack *v1alpha1.Stack) (bool, error) {
188203
_, err := h.getStack(stack)
189204
if err != nil {

0 commit comments

Comments
 (0)