From 7246dfa7f0e40a49253a80ee7e60110afd8941bd Mon Sep 17 00:00:00 2001 From: Pavel Sinkevych Date: Thu, 1 Nov 2018 16:59:31 +0300 Subject: [PATCH 1/2] Manage CloudFromation Stack tags --- README.md | 51 ++++++++++++++++ cmd/cloudformation-operator/main.go | 31 ++++++++-- docs/img/stack-tags.png | Bin 0 -> 22122 bytes examples/cfs-my-bucket-tags.yaml | 17 ++++++ pkg/apis/cloudformation/v1alpha1/types.go | 3 +- pkg/stub/handler.go | 71 +++++++++++++--------- 6 files changed, 138 insertions(+), 35 deletions(-) create mode 100644 docs/img/stack-tags.png create mode 100644 examples/cfs-my-bucket-tags.yaml diff --git a/README.md b/README.md index 4c6c59b..9d6fc38 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,8 @@ Once running the operator should print some output but shouldn't actually do any # Demo +## Create stack + Currently you don't have any stacks. ```console @@ -104,6 +106,8 @@ status: VoilĂ , you just created a CloudFormation stack by only talking to Kubernetes. +## Update stack + You can also update your stack: Let's change the `VersioningConfiguration` from `Suspended` to `Enabled`: ```yaml @@ -135,6 +139,39 @@ Wait until the operator discovered and executed the change, then look at your AW ![Update stack](docs/img/stack-update.png) +## Tags + +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: +- `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"}' +- `tags` parameter at kubernetes resource spec: +```yaml +apiVersion: cloudformation.linki.space/v1alpha1 +kind: Stack +metadata: + name: my-bucket +spec: + tags: + foo: dataFromStack + template: | + --- + AWSTemplateFormatVersion: '2010-09-09' + + Resources: + S3Bucket: + Type: AWS::S3::Bucket + Properties: + VersioningConfiguration: + Status: Enabled +``` + +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. + +If we run the operation and a `Stack` resource with the described above examples, we'll see such picture: + +![Stack tags](docs/img/stack-tags.png) + +## Parameters + 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*. 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 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. +## Outputs + Furthermore, CloudFormation supports so called `Outputs`. These can be used for dynamic values that are only known after a stack has been created. In our example, we don't define a particular S3 bucket name but instead let AWS generate one for us. @@ -233,6 +272,8 @@ status: 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`. +## Delete stack + 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: ```console @@ -244,6 +285,16 @@ Check your CloudFormation console once more and validate that your stack as well ![Delete stack](docs/img/stack-delete.png) +# Command-line arguments + +Argument | Environment variable | Default value | Description +---------|----------------------|---------------|------------ +debug | DEBUG | | Enable debug logging. +dry-run | DRY_RUN | | If true, don't actually do anything. +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. +namespace | WATCH_NAMESPACE | default | The Kubernetes namespace to watch +region | AWS_REGION | | The AWS region to use + # Cleanup Clean up the resources: diff --git a/cmd/cloudformation-operator/main.go b/cmd/cloudformation-operator/main.go index cbf6798..95cccd5 100644 --- a/cmd/cloudformation-operator/main.go +++ b/cmd/cloudformation-operator/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "runtime" "github.com/alecthomas/kingpin" @@ -17,16 +18,25 @@ import ( ) var ( - namespace string - region string - dryRun bool - debug bool - version = "0.2.0+git" + namespace string + region string + globalTags string + dryRun bool + debug bool + version = "0.2.0+git" ) +type Tags map[string]string + func init() { kingpin.Flag("namespace", "The Kubernetes namespace to watch").Default("default").Envar("WATCH_NAMESPACE").StringVar(&namespace) kingpin.Flag("region", "The AWS region to use").Envar("AWS_REGION").StringVar(®ion) + kingpin.Flag( + "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.", + ).Default("{}").Envar("GLOBAL_TAGS").StringVar(&globalTags) kingpin.Flag("dry-run", "If true, don't actually do anything.").Envar("DRY_RUN").BoolVar(&dryRun) kingpin.Flag("debug", "Enable debug logging.").Envar("DEBUG").BoolVar(&debug) } @@ -38,6 +48,15 @@ func printVersion() { logrus.Infof("cloudformation-operator Version: %v", version) } +func parseTags() map[string]string { + var globalTagsParsed map[string]string + err := json.Unmarshal([]byte(globalTags), &globalTagsParsed) + if err != nil { + logrus.Error("Failed to parse global tags: ", err) + } + return globalTagsParsed +} + func main() { kingpin.Version(version) kingpin.Parse() @@ -57,6 +76,6 @@ func main() { }) sdk.Watch("cloudformation.linki.space/v1alpha1", "Stack", namespace, 0) - sdk.Handle(stub.NewHandler(client, dryRun)) + sdk.Handle(stub.NewHandler(client, parseTags(), dryRun)) sdk.Run(context.TODO()) } diff --git a/docs/img/stack-tags.png b/docs/img/stack-tags.png new file mode 100644 index 0000000000000000000000000000000000000000..00b4da211b88feb540d7986d2a9330e03b4711e4 GIT binary patch literal 22122 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 literal 0 HcmV?d00001 diff --git a/examples/cfs-my-bucket-tags.yaml b/examples/cfs-my-bucket-tags.yaml new file mode 100644 index 0000000..74f3e20 --- /dev/null +++ b/examples/cfs-my-bucket-tags.yaml @@ -0,0 +1,17 @@ +apiVersion: cloudformation.linki.space/v1alpha1 +kind: Stack +metadata: + name: my-bucket +spec: + tags: + foo: dataFromStack + template: | + --- + AWSTemplateFormatVersion: '2010-09-09' + + Resources: + S3Bucket: + Type: AWS::S3::Bucket + Properties: + VersioningConfiguration: + Status: Suspended diff --git a/pkg/apis/cloudformation/v1alpha1/types.go b/pkg/apis/cloudformation/v1alpha1/types.go index 276f1d3..620e761 100644 --- a/pkg/apis/cloudformation/v1alpha1/types.go +++ b/pkg/apis/cloudformation/v1alpha1/types.go @@ -22,8 +22,9 @@ type Stack struct { } type StackSpec struct { - Template string `json:"template"` Parameters map[string]string `json:"parameters"` + Tags map[string]string `json:"tags"` + Template string `json:"template"` } type StackStatus struct { StackID string `json:"stackID"` diff --git a/pkg/stub/handler.go b/pkg/stub/handler.go index 2c20520..34099b4 100644 --- a/pkg/stub/handler.go +++ b/pkg/stub/handler.go @@ -29,12 +29,13 @@ var ( ) type Handler struct { - client cloudformationiface.CloudFormationAPI - dryRun bool + client cloudformationiface.CloudFormationAPI + globalTags map[string]string + dryRun bool } -func NewHandler(client cloudformationiface.CloudFormationAPI, dryRun bool) handler.Handler { - return &Handler{client: client, dryRun: dryRun} +func NewHandler(client cloudformationiface.CloudFormationAPI, globalTags map[string]string, dryRun bool) handler.Handler { + return &Handler{client: client, globalTags: globalTags, dryRun: dryRun} } func (h *Handler) Handle(ctx types.Context, event types.Event) error { @@ -81,25 +82,13 @@ func (h *Handler) createStack(stack *v1alpha1.Stack) error { return nil } - params := []*cloudformation.Parameter{} - for k, v := range stack.Spec.Parameters { - params = append(params, &cloudformation.Parameter{ - ParameterKey: aws.String(k), - ParameterValue: aws.String(v), - }) - } - input := &cloudformation.CreateStackInput{ StackName: aws.String(stack.Name), TemplateBody: aws.String(stack.Spec.Template), - Parameters: params, - Tags: []*cloudformation.Tag{ - { - Key: aws.String(ownerTagKey), - Value: aws.String(ownerTagValue), - }, - }, + Parameters: h.processStackParams(stack), + Tags: h.processStackTags(stack), } + if _, err := h.client.CreateStack(input); err != nil { return err } @@ -119,18 +108,11 @@ func (h *Handler) updateStack(stack *v1alpha1.Stack) error { return nil } - params := []*cloudformation.Parameter{} - for k, v := range stack.Spec.Parameters { - params = append(params, &cloudformation.Parameter{ - ParameterKey: aws.String(k), - ParameterValue: aws.String(v), - }) - } - input := &cloudformation.UpdateStackInput{ StackName: aws.String(stack.Name), TemplateBody: aws.String(stack.Spec.Template), - Parameters: params, + Parameters: h.processStackParams(stack), + Tags: h.processStackTags(stack), } if _, err := h.client.UpdateStack(input); err != nil { @@ -184,6 +166,39 @@ func (h *Handler) getStack(stack *v1alpha1.Stack) (*cloudformation.Stack, error) return resp.Stacks[0], nil } +func (h *Handler) processStackParams(stack *v1alpha1.Stack) ([]*cloudformation.Parameter) { + params := []*cloudformation.Parameter{} + for k, v := range stack.Spec.Parameters { + params = append(params, &cloudformation.Parameter{ + ParameterKey: aws.String(k), + ParameterValue: aws.String(v), + }) + } + return params +} + +func (h *Handler) processStackTags(stack *v1alpha1.Stack) ([]*cloudformation.Tag) { + tags := []*cloudformation.Tag{ + { + Key: aws.String(ownerTagKey), + Value: aws.String(ownerTagValue), + }, + } + for k, v := range h.globalTags { + tags = append(tags, &cloudformation.Tag{ + Key: aws.String(k), + Value: aws.String(v), + }) + } + for k, v := range stack.Spec.Tags { + tags = append(tags, &cloudformation.Tag{ + Key: aws.String(k), + Value: aws.String(v), + }) + } + return tags +} + func (h *Handler) stackExists(stack *v1alpha1.Stack) (bool, error) { _, err := h.getStack(stack) if err != nil { From 5c19c50da9b92b71eddd2dd3e5297c0929c840f1 Mon Sep 17 00:00:00 2001 From: Martin Linkhorst Date: Fri, 2 Nov 2018 11:52:10 +0100 Subject: [PATCH 2/2] ref: change tags parameter from json parsing to string map --- README.md | 6 +- cmd/cloudformation-operator/main.go | 33 +++-------- pkg/stub/handler.go | 86 +++++++++++++++-------------- 3 files changed, 57 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 9d6fc38..d7c1fc7 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ Wait until the operator discovered and executed the change, then look at your AW ## Tags 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: -- `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"}' +- `--tag` command line argument or `AWS_TAGS` environment variable which allows setting default tags for all resources managed by the operator. The format is `--tag=foo=bar --tag=wambo=baz` on the command line or with a line break when specifying as an env var. (e.g. in zsh: `AWS_TAGS="foo=bar"$'\n'"wambo=baz"`) - `tags` parameter at kubernetes resource spec: ```yaml apiVersion: cloudformation.linki.space/v1alpha1 @@ -164,7 +164,7 @@ spec: Status: Enabled ``` -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. +Resource-specific tags have precedence over the default 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. If we run the operation and a `Stack` resource with the described above examples, we'll see such picture: @@ -291,7 +291,7 @@ Argument | Environment variable | Default value | Description ---------|----------------------|---------------|------------ debug | DEBUG | | Enable debug logging. dry-run | DRY_RUN | | If true, don't actually do anything. -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. +tag ... | AWS_TAGS | | Default tags which should be applied for all stacks. The format is `--tag=foo=bar --tag=wambo=baz` on the command line or with a line break when specifying as an env var. (e.g. in zsh: `AWS_TAGS="foo=bar"$'\n'"wambo=baz"`) namespace | WATCH_NAMESPACE | default | The Kubernetes namespace to watch region | AWS_REGION | | The AWS region to use diff --git a/cmd/cloudformation-operator/main.go b/cmd/cloudformation-operator/main.go index 95cccd5..c5cb49e 100644 --- a/cmd/cloudformation-operator/main.go +++ b/cmd/cloudformation-operator/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "encoding/json" "runtime" "github.com/alecthomas/kingpin" @@ -18,25 +17,18 @@ import ( ) var ( - namespace string - region string - globalTags string - dryRun bool - debug bool - version = "0.2.0+git" + namespace string + region string + tags = map[string]string{} + dryRun bool + debug bool + version = "0.3.0+git" ) -type Tags map[string]string - func init() { kingpin.Flag("namespace", "The Kubernetes namespace to watch").Default("default").Envar("WATCH_NAMESPACE").StringVar(&namespace) kingpin.Flag("region", "The AWS region to use").Envar("AWS_REGION").StringVar(®ion) - kingpin.Flag( - "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.", - ).Default("{}").Envar("GLOBAL_TAGS").StringVar(&globalTags) + kingpin.Flag("tag", "Tags to apply to all Stacks by default. Specify multiple times for multiple tags.").Envar("AWS_TAGS").StringMapVar(&tags) kingpin.Flag("dry-run", "If true, don't actually do anything.").Envar("DRY_RUN").BoolVar(&dryRun) kingpin.Flag("debug", "Enable debug logging.").Envar("DEBUG").BoolVar(&debug) } @@ -48,15 +40,6 @@ func printVersion() { logrus.Infof("cloudformation-operator Version: %v", version) } -func parseTags() map[string]string { - var globalTagsParsed map[string]string - err := json.Unmarshal([]byte(globalTags), &globalTagsParsed) - if err != nil { - logrus.Error("Failed to parse global tags: ", err) - } - return globalTagsParsed -} - func main() { kingpin.Version(version) kingpin.Parse() @@ -76,6 +59,6 @@ func main() { }) sdk.Watch("cloudformation.linki.space/v1alpha1", "Stack", namespace, 0) - sdk.Handle(stub.NewHandler(client, parseTags(), dryRun)) + sdk.Handle(stub.NewHandler(client, tags, dryRun)) sdk.Run(context.TODO()) } diff --git a/pkg/stub/handler.go b/pkg/stub/handler.go index 34099b4..7b94c03 100644 --- a/pkg/stub/handler.go +++ b/pkg/stub/handler.go @@ -30,12 +30,12 @@ var ( type Handler struct { client cloudformationiface.CloudFormationAPI - globalTags map[string]string + defautTags map[string]string dryRun bool } -func NewHandler(client cloudformationiface.CloudFormationAPI, globalTags map[string]string, dryRun bool) handler.Handler { - return &Handler{client: client, globalTags: globalTags, dryRun: dryRun} +func NewHandler(client cloudformationiface.CloudFormationAPI, defautTags map[string]string, dryRun bool) handler.Handler { + return &Handler{client: client, defautTags: defautTags, dryRun: dryRun} } func (h *Handler) Handle(ctx types.Context, event types.Event) error { @@ -85,8 +85,8 @@ func (h *Handler) createStack(stack *v1alpha1.Stack) error { input := &cloudformation.CreateStackInput{ StackName: aws.String(stack.Name), TemplateBody: aws.String(stack.Spec.Template), - Parameters: h.processStackParams(stack), - Tags: h.processStackTags(stack), + Parameters: stackParameters(stack), + Tags: stackTags(stack, h.defautTags), } if _, err := h.client.CreateStack(input); err != nil { @@ -111,8 +111,8 @@ func (h *Handler) updateStack(stack *v1alpha1.Stack) error { input := &cloudformation.UpdateStackInput{ StackName: aws.String(stack.Name), TemplateBody: aws.String(stack.Spec.Template), - Parameters: h.processStackParams(stack), - Tags: h.processStackTags(stack), + Parameters: stackParameters(stack), + Tags: stackTags(stack, h.defautTags), } if _, err := h.client.UpdateStack(input); err != nil { @@ -166,39 +166,6 @@ func (h *Handler) getStack(stack *v1alpha1.Stack) (*cloudformation.Stack, error) return resp.Stacks[0], nil } -func (h *Handler) processStackParams(stack *v1alpha1.Stack) ([]*cloudformation.Parameter) { - params := []*cloudformation.Parameter{} - for k, v := range stack.Spec.Parameters { - params = append(params, &cloudformation.Parameter{ - ParameterKey: aws.String(k), - ParameterValue: aws.String(v), - }) - } - return params -} - -func (h *Handler) processStackTags(stack *v1alpha1.Stack) ([]*cloudformation.Tag) { - tags := []*cloudformation.Tag{ - { - Key: aws.String(ownerTagKey), - Value: aws.String(ownerTagValue), - }, - } - for k, v := range h.globalTags { - tags = append(tags, &cloudformation.Tag{ - Key: aws.String(k), - Value: aws.String(v), - }) - } - for k, v := range stack.Spec.Tags { - tags = append(tags, &cloudformation.Tag{ - Key: aws.String(k), - Value: aws.String(v), - }) - } - return tags -} - func (h *Handler) stackExists(stack *v1alpha1.Stack) (bool, error) { _, err := h.getStack(stack) if err != nil { @@ -282,3 +249,42 @@ func (h *Handler) waitWhile(stack *v1alpha1.Stack, status string) error { return nil } } + +// stackParameters converts the parameters field on a Stack resource to CloudFormation Parameters. +func stackParameters(stack *v1alpha1.Stack) []*cloudformation.Parameter { + params := []*cloudformation.Parameter{} + for k, v := range stack.Spec.Parameters { + params = append(params, &cloudformation.Parameter{ + ParameterKey: aws.String(k), + ParameterValue: aws.String(v), + }) + } + return params +} + +// stackTags converts the tags field on a Stack resource to CloudFormation Tags. +// Furthermore, it adds a tag for marking ownership as well as any tags given by defaultTags. +func stackTags(stack *v1alpha1.Stack, defaultTags map[string]string) []*cloudformation.Tag { + // ownership tag + tags := []*cloudformation.Tag{ + { + Key: aws.String(ownerTagKey), + Value: aws.String(ownerTagValue), + }, + } + // default tags + for k, v := range defaultTags { + tags = append(tags, &cloudformation.Tag{ + Key: aws.String(k), + Value: aws.String(v), + }) + } + // tags specified on the Stack resource + for k, v := range stack.Spec.Tags { + tags = append(tags, &cloudformation.Tag{ + Key: aws.String(k), + Value: aws.String(v), + }) + } + return tags +}