diff --git a/README.md b/README.md index 4c6c59b..d7c1fc7 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: +- `--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: @@ -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. +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: diff --git a/cmd/cloudformation-operator/main.go b/cmd/cloudformation-operator/main.go index cbf6798..c5cb49e 100644 --- a/cmd/cloudformation-operator/main.go +++ b/cmd/cloudformation-operator/main.go @@ -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(®ion) + 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) } @@ -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()) } diff --git a/docs/img/stack-tags.png b/docs/img/stack-tags.png new file mode 100644 index 0000000..00b4da2 Binary files /dev/null and b/docs/img/stack-tags.png differ 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..7b94c03 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 + 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 { @@ -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 } @@ -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 { @@ -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 +}