├── .gitignore ├── dynamodb-tfstates.tf ├── main.tf ├── variables.tf ├── LICENSE ├── outputs.tf ├── docs └── usage.md ├── kms-tfstates.tf ├── README.md └── bucket-tfstates.tf /.gitignore: -------------------------------------------------------------------------------- 1 | # OS generated files # 2 | ###################### 3 | .DS_Store 4 | .DS_Store? 5 | ._* 6 | .Spotlight-V100 7 | .Trashes 8 | ehthumbs.db 9 | Thumbs.db 10 | **/.terraform 11 | **/*.tfstate* 12 | .idea 13 | *.iml 14 | *.ipr 15 | *.iws -------------------------------------------------------------------------------- /dynamodb-tfstates.tf: -------------------------------------------------------------------------------- 1 | resource "aws_dynamodb_table" "dynamodb_tfstates" { 2 | name = "${var.bucket_tfstates_name}-lock" 3 | read_capacity = 5 4 | write_capacity = 5 5 | hash_key = "LockID" 6 | 7 | tags = (merge( 8 | map("Name", format("%s", "${var.bucket_tfstates_name}-lock")), 9 | var.tags 10 | )) 11 | 12 | attribute { 13 | name = "LockID" 14 | type = "S" 15 | } 16 | 17 | point_in_time_recovery { 18 | enabled = true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | /** 2 | * Module usage: 3 | * 4 | * module "tfbackend" { 5 | * source = "github.com/claque2000/terraform-aws-tfbackend" 6 | * deploy_region = "eu-west-3" 7 | * tfstates_bucket_name = "terraform-tfstates" 8 | * administrators = ["arn:aws:iam::123456789012:user/admin"] 9 | * users = ["arn:aws:iam::123456789012:role/builder","arn:aws:iam::123456789012:user/dev_x"] 10 | * } 11 | * 12 | */ 13 | 14 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | // Name of the s3 bucket 2 | variable "bucket_tfstates_name" { 3 | type = string 4 | } 5 | 6 | // Region where the objects will be deployed 7 | variable "deploy_region" { 8 | type = string 9 | } 10 | 11 | // List of infrastructure administrators 12 | variable "administrators" { 13 | type = list(any) 14 | } 15 | 16 | // List of Terraform users 17 | variable "users" { 18 | type = list(any) 19 | } 20 | 21 | // Tags to apply on s3_bucket 22 | variable "tags" { 23 | type = map(any) 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Nicolas Diez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | // Line to add in terraform cli to init backend 2 | output "backend_config_cli_string" { 3 | value = "-backend-config='bucket=${var.bucket_tfstates_name}' -backend-config='region=${var.deploy_region}' -backend-config='encrypt=true' -backend-config='kms_key_id=${aws_kms_key.kms_tfstates.arn}' -backend-config='dynamodb_table=${aws_dynamodb_table.dynamodb_tfstates.name}'" 4 | } 5 | 6 | // Map to add in your backend configuration 7 | output "backend_config_map" { 8 | value = { 9 | bucket = var.bucket_tfstates_name 10 | region = var.deploy_region 11 | encrypt = "true" 12 | kms_key_id = aws_kms_key.kms_tfstates.arn 13 | dynamodb_table = aws_dynamodb_table.dynamodb_tfstates.name 14 | } 15 | } 16 | 17 | // ARN of the produced kms key for users' grants 18 | output "kms_tfstates" { 19 | value = aws_kms_key.kms_tfstates.arn 20 | } 21 | 22 | // ARN of the created s3 bucket for usage in policies 23 | output "s3_tfstates" { 24 | value = aws_s3_bucket.bucket_tfstates.arn 25 | } 26 | 27 | // ARN of the dynamodb table created 28 | output "dynamodb_tfstates" { 29 | value = aws_dynamodb_table.dynamodb_tfstates.arn 30 | } -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | Module usage: 2 | 3 | module "tfbackend" { 4 | source = "github.com/claque2000/terraform-aws-tfbackend" 5 | deploy_region = "eu-west-3" 6 | bucket_tfstates_name = "terraform-tfstates" 7 | administrators = ["arn:aws:iam::123456789012:user/admin"] 8 | users = ["arn:aws:iam::123456789012:role/builder","arn:aws:iam::123456789012:user/dev_x"] 9 | tags = ["BillIt"] 10 | } 11 | 12 | 13 | 14 | ## Inputs 15 | 16 | | Name | Description | Type | Default | Required | 17 | |------|-------------|:----:|:-----:|:-----:| 18 | | administrators | List of infrastructure administrators | list | - | yes | 19 | | bucket_tfstates_name | Name of the s3 bucket | string | - | yes | 20 | | deploy_region | Region where the objects will be deployed | string | - | yes | 21 | | tags | Tags to apply on s3_bucket | map | - | yes | 22 | | users | List of Terraform users | list | - | yes | 23 | 24 | ## Outputs 25 | 26 | | Name | Description | 27 | |------|-------------| 28 | | backend_config_cli_string | Line to add in terraform cli to init backend | 29 | | backend_config_map | Map to add in your backend configuration | 30 | | dynamodb_tfstates | ARN of the dynamodb table created | 31 | | kms_tfstates | ARN of the produced kms key for users' grants | 32 | | s3_tfstates | ARN of the created s3 bucket for usage in policies | 33 | 34 | -------------------------------------------------------------------------------- /kms-tfstates.tf: -------------------------------------------------------------------------------- 1 | resource "aws_kms_key" "kms_tfstates" { 2 | description = "Terraform tfstates bucket encryption key" 3 | policy = data.aws_iam_policy_document.kms_tfstate_policy.json 4 | 5 | lifecycle { 6 | prevent_destroy = true 7 | } 8 | } 9 | 10 | data "aws_iam_policy_document" "kms_tfstate_policy" { 11 | statement { 12 | sid = "Allow access for Key Administrators" 13 | 14 | principals { 15 | type = "AWS" 16 | identifiers = [var.administrators] 17 | } 18 | 19 | actions = [ 20 | "kms:Create*", 21 | "kms:Describe*", 22 | "kms:Enable*", 23 | "kms:List*", 24 | "kms:Put*", 25 | "kms:Update*", 26 | "kms:Revoke*", 27 | "kms:Disable*", 28 | "kms:Get*", 29 | "kms:Delete*", 30 | "kms:TagResource", 31 | "kms:UntagResource", 32 | "kms:ScheduleKeyDeletion", 33 | "kms:CancelKeyDeletion", 34 | ] 35 | 36 | resources = ["*"] 37 | } 38 | 39 | statement { 40 | sid = "Allow use of the key" 41 | 42 | principals { 43 | type = "AWS" 44 | identifiers = [var.administrators, var.users] 45 | } 46 | 47 | actions = [ 48 | "kms:Encrypt", 49 | "kms:Decrypt", 50 | "kms:ReEncrypt*", 51 | "kms:GenerateDataKey*", 52 | "kms:DescribeKey", 53 | "kms:List*", 54 | ] 55 | 56 | resources = ["*"] 57 | } 58 | } 59 | 60 | resource "aws_kms_alias" "tfstates" { 61 | name = "alias/${var.bucket_tfstates_name}" 62 | target_key_id = aws_kms_key.kms_tfstates.key_id 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Module to create tfstate backend 2 | ## Why ? 3 | As you should know, you can use s3 as backend for your tfstates. That's great but there is some more features availables: 4 | * s3 encryption with kms for securing your tfstates at rest 5 | * dynamodb lock to ensure non parallel execution of the same stack 6 | But it can be quite annoying to create it at each new project. So, this module was created to fullfil this need. 7 | 8 | ## What is included ? 9 | This modules will create all the necessary pieces you need to have all those features (assuming you have an admin and a user or more with whole access to bucket): 10 | * a kms key where: 11 | * the admin can update the key policy 12 | * the user can use the key to encrypt/decrypt data 13 | * an s3 bucket: 14 | * with write access for users 15 | * denying unencrypted uploads 16 | * a dynamodb table which will be used to lock multiple deployments 17 | 18 | Differents outputs are available from this module: 19 | * the cli arguments to use during terraform init 20 | * the configuration to add in your backend section 21 | 22 | ## How to use it ? 23 | See [Usage](docs/usage.md). 24 | 25 | You will also need to add policy statement for kms usage to your users, using the KMS key arn given in module output: 26 | ``` 27 | { 28 | "Effect": "Allow", 29 | "Action": [ 30 | "kms:Encrypt", 31 | "kms:Decrypt", 32 | "kms:GenerateDataKey" 33 | ], 34 | "Resource": output kms arn 35 | } 36 | ``` 37 | You need also to grant access to the dynamodb table: 38 | ``` 39 | { 40 | "Effect": "Allow", 41 | "Action": [ 42 | "dynamodb:PutItem", 43 | "dynamodb:GetItem", 44 | "dynamodb:DeleteItem" 45 | ], 46 | "Resource": output dynamodb arn 47 | } 48 | ``` 49 | 50 | ## We love OSS 51 | This module is distributed under [MIT License](https://opensource.org/licenses/MIT). 52 | 53 | The usage documentation was created using [Terraform-docs](https://github.com/segmentio/terraform-docs) 54 | 55 | 56 | Thanks to [Aurélien Maury](https://github.com/aurelienmaury) for his code review 57 | -------------------------------------------------------------------------------- /bucket-tfstates.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "bucket_tfstates" { 2 | bucket = var.bucket_tfstates_name 3 | acl = "private" 4 | 5 | versioning { 6 | enabled = true 7 | } 8 | 9 | force_destroy = true 10 | 11 | server_side_encryption_configuration { 12 | rule { 13 | apply_server_side_encryption_by_default { 14 | kms_master_key_id = aws_kms_key.kms_tfstates.arn 15 | sse_algorithm = "aws:kms" 16 | } 17 | } 18 | } 19 | 20 | policy = data.aws_iam_policy_document.bucket_tfstates_policy.json 21 | 22 | tags = merge( 23 | map("Name", var.bucket_tfstates_name), 24 | var.tags 25 | ) 26 | 27 | lifecycle { 28 | prevent_destroy = true 29 | } 30 | } 31 | 32 | data "aws_iam_policy_document" "bucket_tfstates_policy" { 33 | statement { 34 | sid = "Admins can do everything" 35 | effect = "Allow" 36 | 37 | principals { 38 | identifiers = [var.administrators] 39 | type = "AWS" 40 | } 41 | 42 | actions = [ 43 | "s3:DeleteBucket", 44 | "s3:ListBucket", 45 | "s3:PutObject", 46 | "s3:GetObject", 47 | "s3:DeleteObject", 48 | ] 49 | 50 | resources = [ 51 | "arn:aws:s3:::${var.bucket_tfstates_name}", 52 | "arn:aws:s3:::${var.bucket_tfstates_name}/*", 53 | ] 54 | } 55 | 56 | statement { 57 | sid = "Users can run Terraform" 58 | effect = "Allow" 59 | 60 | principals { 61 | identifiers = [var.users] 62 | type = "AWS" 63 | } 64 | 65 | actions = [ 66 | "s3:ListBucket", 67 | "s3:PutObject", 68 | "s3:GetObject", 69 | ] 70 | 71 | resources = [ 72 | "arn:aws:s3:::${var.bucket_tfstates_name}", 73 | "arn:aws:s3:::${var.bucket_tfstates_name}/*", 74 | ] 75 | } 76 | 77 | statement { 78 | sid = "Deny unencrypted object upload" 79 | effect = "Deny" 80 | 81 | principals { 82 | identifiers = ["*"] 83 | type = "AWS" 84 | } 85 | 86 | actions = ["s3:PutObject"] 87 | resources = ["arn:aws:s3:::${var.bucket_tfstates_name}/*"] 88 | 89 | condition { 90 | test = "StringNotEquals" 91 | values = ["aws:kms"] 92 | variable = "s3:x-amz-server-side-encryption" 93 | } 94 | } 95 | } 96 | --------------------------------------------------------------------------------