diff --git a/.gitignore b/.gitignore index 41859c8..c5a6503 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ # Module directory .terraform/ +.idea +terraform-aws-tfstate-backend.iml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..241026e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,16 @@ +addons: + apt: + packages: + - git + - make + - curl + +install: + - make init + +script: + - make terraform/install + - make terraform/get-plugins + - make terraform/get-modules + - make terraform/lint + - make terraform/validate diff --git a/LICENSE b/LICENSE index 261eeb9..c37833f 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2018 Cloud Posse, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d002c7d --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +SHELL := /bin/bash + +-include $(shell curl -sSL -o .build-harness "https://git.io/build-harness"; echo .build-harness) + +lint: + $(SELF) terraform/install terraform/get-modules terraform/get-plugins terraform/lint terraform/validate diff --git a/README.md b/README.md index 9e11c15..e069b99 100644 --- a/README.md +++ b/README.md @@ -1 +1,175 @@ -# terraform-aws-state-backend \ No newline at end of file +# terraform-aws-tfstate-backend [![Build Status](https://travis-ci.org/cloudposse/terraform-aws-tfstate-backend.svg?branch=master)](https://travis-ci.org/cloudposse/terraform-aws-tfstate-backend) + +Terraform module to provision an S3 bucket to store `terraform.tfstate` file and a DynamoDB table to lock the state file +to prevent concurrent modifications and state corruption. + +The module supports the following: + +1. Forced server-side encryption at rest for the S3 bucket +2. S3 bucket versioning to allow for Terraform state recovery in the case of accidental deletions and human errors +3. State locking and consistency checking via DynamoDB table to prevent concurrent operations +4. DynamoDB server-side encryption + +https://www.terraform.io/docs/backends/types/s3.html + + +__NOTE:__ The operators of the module (IAM Users) must have permissions to create S3 buckets and DynamoDB tables when performing `terraform plan` and `terraform apply` + + +## Usage + +```hcl +terraform { + required_version = ">= 0.11.3" +} + +module "terraform_state_backend" { + source = "git::https://github.com/cloudposse/terraform-aws-tfstate-backend.git?ref=master" + namespace = "cp" + stage = "dev" + name = "app" + region = "us-east-1" +} +``` + +__NOTE:__ First create the bucket and table without any state enabled (Terraform will use the local file system to store state). +You can then import the bucket and table by using [`terraform import`](https://www.terraform.io/docs/import/index.html) and store the state file into the bucket. + +Once the bucket and table have been created, configure the [backend](https://www.terraform.io/docs/backends/types/s3.html) + +```hcl +terraform { + required_version = ">= 0.11.3" + + backend "s3" { + region = "us-east-1" + bucket = "< the name of the S3 bucket >" + key = "terraform.tfstate" + dynamodb_table = "< the name of the DynamoDB table >" + encrypt = true + } +} + +module "another_module" { + source = "....." +} +``` + +Initialize the backend with `terraform init`. + +After `terraform apply`, `terraform.tfstate` file will be stored in the bucket, +and the DynamoDB table will be used to lock the state to prevent concurrent modifications. + +
+ +![s3-bucket-with-terraform-state](images/s3-bucket-with-terraform-state.png) + + +## Variables + +| Name | Default | Description | Required | +|:-------------------------|:-------------|:----------------------------------------------------------------------------------|:--------:| +| `namespace` | `` | Namespace (_e.g._ `cp` or `cloudposse`) | Yes | +| `stage` | `` | Stage (_e.g._ `prod`, `dev`, `staging`) | Yes | +| `region` | `us-east-1` | AWS Region the S3 bucket should reside in | Yes | +| `name` | `terraform` | Name (_e.g._ `app`, `cluster`, or `terraform`) | No | +| `attributes` | `["state"]` | Additional attributes (_e.g._ `policy` or `role`) | No | +| `tags` | `{}` | Additional tags (_e.g._ `map("BusinessUnit","XYZ")` | No | +| `delimiter` | `-` | Delimiter to be used between `namespace`, `stage`, `name`, and `attributes` | No | +| `acl` | `private` | The canned ACL to apply to the S3 bucket | No | +| `read_capacity` | `5` | DynamoDB read capacity units | No | +| `write_capacity` | `5` | DynamoDB write capacity units | No | + + +## Outputs + +| Name | Description | +|:-------------------------|:-----------------------------| +| `s3_bucket_domain_name` | S3 bucket domain name | +| `s3_bucket_id` | S3 bucket ID | +| `s3_bucket_arn` | S3 bucket ARN | +| `dynamodb_table_id` | DynamoDB table ID | +| `dynamodb_table_arn` | DynamoDB table ARN | +| `dynamodb_table_name` | DynamoDB table name | + + +## Help + +**Got a question?** + +File a GitHub [issue](https://github.com/cloudposse/terraform-aws-tfstate-backend/issues), send us an [email](mailto:hello@cloudposse.com) or reach out to us on [Gitter](https://gitter.im/cloudposse/). + + +## Contributing + +### Bug Reports & Feature Requests + +Please use the [issue tracker](https://github.com/cloudposse/terraform-aws-tfstate-backend/issues) to report any bugs or file feature requests. + +### Developing + +If you are interested in being a contributor and want to get involved in developing `terraform-aws-tfstate-backend`, we would love to hear from you! Shoot us an [email](mailto:hello@cloudposse.com). + +In general, PRs are welcome. We follow the typical "fork-and-pull" Git workflow. + + 1. **Fork** the repo on GitHub + 2. **Clone** the project to your own machine + 3. **Commit** changes to your own branch + 4. **Push** your work back up to your fork + 5. Submit a **Pull request** so that we can review your changes + +**NOTE:** Be sure to merge the latest from "upstream" before making a pull request! + + +## License + +[APACHE 2.0](LICENSE) © 2018 [Cloud Posse, LLC](https://cloudposse.com) + +See [LICENSE](LICENSE) for full details. + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + + +## About + +`terraform-aws-tfstate-backend` is maintained and funded by [Cloud Posse, LLC][website]. + +![Cloud Posse](https://cloudposse.com/logo-300x69.png) + + +Like it? Please let us know at + +We love [Open Source Software](https://github.com/cloudposse/)! + +See [our other projects][community] +or [hire us][hire] to help build your next cloud platform. + + [website]: https://cloudposse.com/ + [community]: https://github.com/cloudposse/ + [hire]: https://cloudposse.com/contact/ + + +### Contributors + +| [![Erik Osterman][erik_img]][erik_web]
[Erik Osterman][erik_web] | [![Andriy Knysh][andriy_img]][andriy_web]
[Andriy Knysh][andriy_web] | +|-------------------------------------------------------|------------------------------------------------------------------| + + [erik_img]: http://s.gravatar.com/avatar/88c480d4f73b813904e00a5695a454cb?s=144 + [erik_web]: https://github.com/osterman/ + [andriy_img]: https://avatars0.githubusercontent.com/u/7356997?v=4&u=ed9ce1c9151d552d985bdf5546772e14ef7ab617&s=144 + [andriy_web]: https://github.com/aknysh/ diff --git a/images/s3-bucket-with-terraform-state.png b/images/s3-bucket-with-terraform-state.png new file mode 100644 index 0000000..09624a5 Binary files /dev/null and b/images/s3-bucket-with-terraform-state.png differ diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..2e54bcf --- /dev/null +++ b/main.tf @@ -0,0 +1,62 @@ +module "s3_bucket_label" { + source = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.3.3" + namespace = "${var.namespace}" + stage = "${var.stage}" + name = "${var.name}" + delimiter = "${var.delimiter}" + attributes = "${var.attributes}" + tags = "${var.tags}" +} + +resource "aws_s3_bucket" "default" { + bucket = "${module.s3_bucket_label.id}" + acl = "${var.acl}" + region = "${var.region}" + force_destroy = false + + versioning { + enabled = true + } + + server_side_encryption_configuration { + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } + } + + tags = "${module.s3_bucket_label.tags}" +} + +module "dynamodb_table_label" { + source = "git::https://github.com/cloudposse/terraform-null-label.git?ref=tags/0.3.3" + namespace = "${var.namespace}" + stage = "${var.stage}" + name = "${var.name}" + delimiter = "${var.delimiter}" + attributes = ["${compact(concat(var.attributes, list("lock")))}"] + tags = "${var.tags}" +} + +resource "aws_dynamodb_table" "default" { + name = "${module.dynamodb_table_label.id}" + read_capacity = "${var.read_capacity}" + write_capacity = "${var.write_capacity}" + hash_key = "LockID" # https://www.terraform.io/docs/backends/types/s3.html#dynamodb_table + + server_side_encryption { + enabled = true + } + + lifecycle { + ignore_changes = ["read_capacity", "write_capacity"] + } + + attribute { + name = "LockID" + type = "S" + } + + tags = "${module.dynamodb_table_label.tags}" +} diff --git a/output.tf b/output.tf new file mode 100644 index 0000000..40f52c4 --- /dev/null +++ b/output.tf @@ -0,0 +1,23 @@ +output "s3_bucket_domain_name" { + value = "${aws_s3_bucket.default.bucket_domain_name}" +} + +output "s3_bucket_id" { + value = "${aws_s3_bucket.default.id}" +} + +output "s3_bucket_arn" { + value = "${aws_s3_bucket.default.arn}" +} + +output "dynamodb_table_name" { + value = "${aws_dynamodb_table.default.name}" +} + +output "dynamodb_table_id" { + value = "${aws_dynamodb_table.default.id}" +} + +output "dynamodb_table_arn" { + value = "${aws_dynamodb_table.default.arn}" +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..c02e4e7 --- /dev/null +++ b/variables.tf @@ -0,0 +1,55 @@ +variable "namespace" { + type = "string" + description = "Namespace (e.g. `cp` or `cloudposse`)" +} + +variable "stage" { + type = "string" + description = "Stage (e.g. `prod`, `dev`, `staging`, `infra`)" +} + +variable "name" { + type = "string" + default = "terraform" + description = "Name (e.g. `app` or `cluster`)" +} + +variable "delimiter" { + type = "string" + default = "-" + description = "Delimiter to be used between `namespace`, `stage`, `name`, and `attributes`" +} + +variable "attributes" { + type = "list" + default = ["state"] + description = "Additional attributes (e.g. `policy` or `role`)" +} + +variable "tags" { + type = "map" + default = {} + description = "Additional tags (e.g. map('BusinessUnit`,`XYZ`)" +} + +variable "region" { + type = "string" + description = "AWS Region the S3 bucket should reside in" + default = "us-east-1" +} + +variable "acl" { + type = "string" + description = "The canned ACL to apply to the S3 bucket" + default = "private" +} + +variable "read_capacity" { + default = 5 + description = "DynamoDB read capacity units" +} + +variable "write_capacity" { + default = 5 + description = "DynamoDB write capacity units" +}