Skip to content

Commit

Permalink
Merge pull request #17 from StarOfService/feature/tags
Browse files Browse the repository at this point in the history
Manage CloudFromation Stack tags
  • Loading branch information
linki authored Nov 2, 2018
2 parents 065f2a4 + 5c19c50 commit 7dd04e7
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 31 deletions.
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
- `--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
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 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:

![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:
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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.
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

# Cleanup

Clean up the resources:
Expand Down
6 changes: 4 additions & 2 deletions cmd/cloudformation-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,16 @@ import (
var (
namespace string
region string
tags = map[string]string{}
dryRun bool
debug bool
version = "0.2.0+git"
version = "0.3.0+git"
)

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(&region)
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)
}
Expand Down Expand Up @@ -57,6 +59,6 @@ func main() {
})

sdk.Watch("cloudformation.linki.space/v1alpha1", "Stack", namespace, 0)
sdk.Handle(stub.NewHandler(client, dryRun))
sdk.Handle(stub.NewHandler(client, tags, dryRun))
sdk.Run(context.TODO())
}
Binary file added docs/img/stack-tags.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions examples/cfs-my-bucket-tags.yaml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion pkg/apis/cloudformation/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
77 changes: 49 additions & 28 deletions pkg/stub/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ var (
)

type Handler struct {
client cloudformationiface.CloudFormationAPI
dryRun bool
client cloudformationiface.CloudFormationAPI
defautTags 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, 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 {
Expand Down Expand Up @@ -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: stackParameters(stack),
Tags: stackTags(stack, h.defautTags),
}

if _, err := h.client.CreateStack(input); err != nil {
return err
}
Expand All @@ -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: stackParameters(stack),
Tags: stackTags(stack, h.defautTags),
}

if _, err := h.client.UpdateStack(input); err != nil {
Expand Down Expand Up @@ -267,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
}

0 comments on commit 7dd04e7

Please sign in to comment.