|
| 1 | +# CloudFormation Custom Resource - Terraform Integration |
| 2 | + |
| 3 | +## Rationale |
| 4 | + |
| 5 | +The integration of Terraform within a CloudFormation Custom Resource addresses the need for greater flexibility in infrastructure management. While AWS CloudFormation is great for managing AWS resources, it's confined to the AWS ecosystem. Terraform, however, offers a wide range of providers, enabling seamless management of resources from third-party services. These include on-premises servers, external databases, monitoring tools, and CI/CD systems. This approach unifies resource management by leveraging CloudFormation's native AWS integration while extending its capabilities through Terraform's extensive provider ecosystem. The result is a more comprehensive and automated infrastructure provisioning process. |
| 6 | + |
| 7 | +## Solution Overview |
| 8 | + |
| 9 | +This solution leverages Lambda-backed CloudFormation Custom Resources to seamlessly integrate Terraform configurations into CloudFormation templates. It offers a Lambda function—designated as the ServiceToken in custom resources—that executes Terraform's provisioning logic. |
| 10 | + |
| 11 | +See [CloudFormation Custom Resources Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html) for more details. |
| 12 | + |
| 13 | +## Setup Instructions |
| 14 | + |
| 15 | +1. Clone the repository: |
| 16 | + |
| 17 | + ```bash |
| 18 | + git clone https://github.com/humanonymous/cloudformation-custom-resource-terraform.git |
| 19 | + cd cloudformation-custom-resource-terraform |
| 20 | + ``` |
| 21 | + |
| 22 | +2. Install **go-task** from [https://taskfile.dev](https://taskfile.dev/). It's a standalone binary with no dependencies that elegantly wraps shell scripts. For more details, refer to [Taskfile.yaml](Taskfile.yaml). You can also copy scripts from the file and run them manually—the code is pretty straightforward. |
| 23 | +3. Configure [Taskfile.env](Taskfile.env) to set up your environment variables. These variables specify function name, S3 bucket used for CloudFormation stack deployment, and other essential parameters. Refer to the file for more details. |
| 24 | +4. Execute the following commands to build and deploy the CloudFormation Custom Resource: |
| 25 | +
|
| 26 | + ```bash |
| 27 | + task clean build deploy --silent |
| 28 | + ``` |
| 29 | +
|
| 30 | +## General Usage |
| 31 | +
|
| 32 | +Create CloudFormation Custom Resources in your infrastructure code. Ensure you use the Lambda ARN in the custom resource configuration's `ServiceToken` property. |
| 33 | + |
| 34 | +```yaml |
| 35 | +Resources: |
| 36 | + CustomTerraformConfigurationExample: |
| 37 | + Type: Custom::TerraformConfiguration |
| 38 | + Properties: |
| 39 | + ServiceToken: <ARN of Lambda> |
| 40 | + Configuration: | |
| 41 | + terraform { |
| 42 | + backend "s3" { |
| 43 | + bucket = "mybucket" |
| 44 | + key = "path/to/my/key" |
| 45 | + region = "us-east-1" |
| 46 | + dynamodb_table = "TableName" |
| 47 | + } |
| 48 | + } |
| 49 | + resource "aws_s3_bucket" "example" { |
| 50 | + bucket = "example-bucket" |
| 51 | + } |
| 52 | + ... |
| 53 | +``` |
| 54 | + |
| 55 | +## Property Reference |
| 56 | + |
| 57 | +### ServiceToken (Required) |
| 58 | + |
| 59 | +The ARN of the Lambda function handling the custom resource logic. The ARN is generated during setup and must be always provided in the custom resource definitions. |
| 60 | + |
| 61 | +```yaml |
| 62 | +Resources: |
| 63 | + CustomTerraformConfigurationExample: |
| 64 | + Type: Custom::TerraformConfiguration |
| 65 | + Properties: |
| 66 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 67 | + # Other properties... |
| 68 | +``` |
| 69 | + |
| 70 | +**Note:** The `ServiceToken` property can't be changed after resource creation. Check the "Potential Drawbacks or Limitations" section for more details. |
| 71 | +
|
| 72 | +### Backend (Optional) |
| 73 | +
|
| 74 | +Specifies the Terraform backend. Supported options: |
| 75 | +
|
| 76 | +- `Auto`: We currently support automatic configuration of an S3 backend with a DynamoDB table for state locking. This is the recommended method for implementing consistent Terraform state isolation per CloudFormation resource. To use the `Auto` configured backend, you'll need to set two additional environment variables in [Taskfile.env](Taskfile.env): |
| 77 | + - `STACK_PARAM_TERRAFORM_BACKEND_AUTO_S3_BUCKET` - The bucket to store Terraform states. This bucket should be accessible by CloudFormation during stack deployment. |
| 78 | + - `STACK_PARAM_TERRAFORM_BACKEND_AUTO_S3_DYNAMODB_TABLE` - The DynamoDB table for state locking and consistency checking. |
| 79 | +- **Partial Configuration**: Provide a partial Terraform backend configuration as defined in the [Backend Partial Configuration](https://developer.hashicorp.com/terraform/language/backend#partial-configuration) guide. |
| 80 | +- **Inline Configuration:** You can also specify backend configuration directly in the `Configuration` property (see the corresponding section below). |
| 81 | + |
| 82 | +**Note:** If not specified, Terraform will use a `local` backend. This prevents the custom resource from preserving state between **Create**, **Update**, and **Delete** actions, leading to CloudFormation lifecycle failures. Always provide a backend configuration to ensure proper state management. |
| 83 | + |
| 84 | +- Automatically configured backend. The `terraform { backend “s3” {}}` block is required. |
| 85 | +
|
| 86 | + ```yaml |
| 87 | + Resources: |
| 88 | + CustomTerraformConfigurationExample: |
| 89 | + Type: Custom::TerraformConfiguration |
| 90 | + Properties: |
| 91 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 92 | + Backend: "Auto" |
| 93 | + Configuration: | |
| 94 | + terraform { |
| 95 | + backend "s3" {} |
| 96 | + } |
| 97 | + resource "aws_s3_bucket" "example" { |
| 98 | + bucket = "example-bucket" |
| 99 | + } |
| 100 | + ``` |
| 101 | +
|
| 102 | +- Partial backend configuration |
| 103 | +
|
| 104 | + ```yaml |
| 105 | + Resources: |
| 106 | + CustomTerraformConfigurationExample: |
| 107 | + Type: Custom::TerraformConfiguration |
| 108 | + Properties: |
| 109 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 110 | + Backend: | |
| 111 | + address = "demo.consul.io" |
| 112 | + scheme = "https" |
| 113 | + path = "path/to/terraform/state" |
| 114 | + Configuration: | |
| 115 | + terraform { |
| 116 | + backend "consul" {} |
| 117 | + } |
| 118 | + resource "aws_s3_bucket" "example" { |
| 119 | + bucket = "example-bucket" |
| 120 | + } |
| 121 | + ``` |
| 122 | +
|
| 123 | +- Inline backend configuration |
| 124 | +
|
| 125 | + ```yaml |
| 126 | + Resources: |
| 127 | + CustomTerraformConfigurationExample: |
| 128 | + Type: Custom::TerraformConfiguration |
| 129 | + Properties: |
| 130 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 131 | + Configuration: | |
| 132 | + terraform { |
| 133 | + backend "consul" { |
| 134 | + address = "demo.consul.io" |
| 135 | + scheme = "https" |
| 136 | + path = "path/to/terraform/state" |
| 137 | + } |
| 138 | + } |
| 139 | + resource "aws_s3_bucket" "example" { |
| 140 | + bucket = "example-bucket" |
| 141 | + } |
| 142 | + ``` |
| 143 | +
|
| 144 | +### Environment (Optional) |
| 145 | +
|
| 146 | +A map of environment variables to be set during Terraform execution. For more details, refer to [Terraform's Environment Variables Guide](https://developer.hashicorp.com/terraform/cli/config/environment-variables). |
| 147 | +
|
| 148 | +```yaml |
| 149 | +Resources: |
| 150 | + CustomTerraformConfigurationExample: |
| 151 | + Type: Custom::TerraformConfiguration |
| 152 | + Properties: |
| 153 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 154 | + Backend: "Auto" |
| 155 | + Environment: |
| 156 | + TF_LOG: "TRACE" |
| 157 | + TF_VAR_bucket_name: "example-bucket" |
| 158 | + Configuration: | |
| 159 | + terraform { |
| 160 | + backend "s3" {} |
| 161 | + } |
| 162 | + variable "bucket_name" { |
| 163 | + type = string |
| 164 | + } |
| 165 | + resource "aws_s3_bucket" "example" { |
| 166 | + bucket = var.bucket_name |
| 167 | + } |
| 168 | +``` |
| 169 | +
|
| 170 | +**Note:** The `TF_IN_AUTOMATION` and `TF_INPUT` environment variables are automatically set and cannot be overridden for Custom CloudFormation Resources. |
| 171 | +
|
| 172 | +### Configuration (Required) |
| 173 | +
|
| 174 | +Specifies the Terraform configuration in one of the following formats: |
| 175 | +
|
| 176 | +- Inline Terraform configuration: |
| 177 | +
|
| 178 | + ```yaml |
| 179 | + Resources: |
| 180 | + CustomTerraformConfigurationExample: |
| 181 | + Type: Custom::TerraformConfiguration |
| 182 | + Properties: |
| 183 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 184 | + Backend: "Auto" |
| 185 | + Configuration: | |
| 186 | + terraform { |
| 187 | + backend "s3" {} |
| 188 | + } |
| 189 | + resource "aws_s3_bucket" "example" { |
| 190 | + bucket = "example-bucket" |
| 191 | + } |
| 192 | + ``` |
| 193 | +
|
| 194 | +- HTTP(S) URL pointing to a Terraform configuration file: |
| 195 | +
|
| 196 | + ```yaml |
| 197 | + Resources: |
| 198 | + CustomTerraformConfigurationExample: |
| 199 | + Type: Custom::TerraformConfiguration |
| 200 | + Properties: |
| 201 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 202 | + Backend: "Auto" |
| 203 | + Configuration: "https://example.com/path/to/terraform.tf" |
| 204 | + ``` |
| 205 | +
|
| 206 | +- S3 URL pointing to a Terraform configuration file: |
| 207 | +
|
| 208 | + ```yaml |
| 209 | + Resources: |
| 210 | + CustomTerraformConfigurationExample: |
| 211 | + Type: Custom::TerraformConfiguration |
| 212 | + Properties: |
| 213 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 214 | + Backend: "Auto" |
| 215 | + Configuration: "s3://example/path/to/terraform.tf" |
| 216 | + ``` |
| 217 | +
|
| 218 | +### Variables (Optional) |
| 219 | +
|
| 220 | +A map of Terraform variables to pass to the configuration: |
| 221 | +
|
| 222 | +```yaml |
| 223 | +Resources: |
| 224 | + CustomTerraformConfigurationExample: |
| 225 | + Type: Custom::TerraformConfiguration |
| 226 | + Properties: |
| 227 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 228 | + Backend: "Auto" |
| 229 | + Configuration: | |
| 230 | + terraform { |
| 231 | + backend "s3" {} |
| 232 | + } |
| 233 | + variable "String" { |
| 234 | + type = string |
| 235 | + } |
| 236 | + variable "List" { |
| 237 | + type = list |
| 238 | + } |
| 239 | + variable "Number" { |
| 240 | + type = number |
| 241 | + } |
| 242 | + resource "aws_s3_bucket" "example" { |
| 243 | + bucket = var.String |
| 244 | + } |
| 245 | + Variables: |
| 246 | + String: "example-bucket" |
| 247 | + List: |
| 248 | + - "item1" |
| 249 | + - "item2" |
| 250 | + Number: 5 |
| 251 | +``` |
| 252 | +
|
| 253 | +### ExecutionLogsTargetArn (Optional) |
| 254 | +
|
| 255 | +The ARN of a CloudWatch Log Group where Terraform execution logs will be sent. |
| 256 | +
|
| 257 | +## Outputs |
| 258 | +
|
| 259 | +The Custom Resource automatically maps Terraform outputs to CloudFormation outputs, allowing seamless integration between Terraform-managed resources and the rest of your CloudFormation stack. |
| 260 | +
|
| 261 | +```yaml |
| 262 | +Resources: |
| 263 | + CustomTerraformConfigurationExample: |
| 264 | + Type: Custom::TerraformConfiguration |
| 265 | + Properties: |
| 266 | + ServiceToken: "arn:aws:lambda:us-east-1:123456789012:function:cloudformation-custom-resource-terraform" |
| 267 | + Backend: "Auto" |
| 268 | + Configuration: | |
| 269 | + terraform { |
| 270 | + backend "s3" {} |
| 271 | + } |
| 272 | + resource "aws_s3_bucket" "example" { |
| 273 | + bucket = "example-bucket" |
| 274 | + } |
| 275 | + output "S3BucketName" { |
| 276 | + value = var.aws_s3_bucket.id |
| 277 | + } |
| 278 | +
|
| 279 | +Outputs: |
| 280 | + S3BucketName: !GetAtt TerraformManagedResource.S3BucketName |
| 281 | +``` |
| 282 | +
|
| 283 | +## Potential Drawbacks or Limitations |
| 284 | +
|
| 285 | +- Modifying the `ServiceToken` value in custom resources requires replacing the entire custom resource. CloudFormation doesn't allow changes to this property and will fail with the error "Modifying service token is not allowed." This limitation means you'll need to recreate the resource if you want to change the underlying Lambda function. |
| 286 | +- Limited access to Terraform state can make troubleshooting difficult. Developers may struggle to diagnose issues or track changes without direct access to the state managed by the CloudFormation Custom Resource. |
| 287 | +
|
| 288 | +## Testing |
| 289 | +
|
| 290 | +1. Install **go-task** from [https://taskfile.dev](https://taskfile.dev/). |
| 291 | +2. Configure the relevant section of [Taskfile.env](Taskfile.env) to set up your environment variables. |
| 292 | +3. Execute the following commands to build and deploy the CloudFormation Custom Resource: |
| 293 | +
|
| 294 | +```bash |
| 295 | +task test --silent |
| 296 | +``` |
0 commit comments