summaryrefslogtreecommitdiffhomepage
path: root/hcl.html.markdown
blob: 4ab2684217ec2bc9b1ee4ea7f0a7e18d8134b6b6 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
---
category: tool
tool: HCL
contributors:
    - ["Romans Malinovskis" , "http://github.com/romaninsh"]
filename: terraform.txt
---
## Introduction

HCL (Hashicorp Configuration Language) is a high-level configuration language used in tools from
Hashicorp (such as Terraform). HCL/Terraform is widely used in provisioning cloud infastructure and
configuring platforms/services through APIs. This document focuses on HCL 0.13 syntax.

HCL is a declarative language and Terraform will consume all `*.tf` files in the current folder, so code
placement and sequence has no significance. Sub-folders can be consumed through modules.

This guide is focused on HCL specifics, you should already be familiar with what Terraform is.

```terraform
// Top-level HCL file will interactively ask user values for the variables
// which do not have a default value
variable "ready" {
  description = "Ready to learn?"
  type = bool
  // default = true
}

// Module block consults a specified folder for *.tf files, would
// effectively prefix all resources IDs with "module.learn-basics."
module "learn-basics" {
  source = "./learn-basics"
  ready_to_learn = var.ready
}

output "knowledge" {
  value = module.learn-basics.knowledge
}
```

## learn-basics

```terraform
// Variables are not automatically passed into modules
// and can be typeless.
variable "ready" {
}

// It is good practice to define a type though. There are 3 primitive types -
// 3 collection types and 2 structural types. Structural types define
// types recursively
variable "structural-types" {
  type = object({
    object: object({
      can-be-nested: bool
    }),
    tuple: tuple([int, string])
  })

  default = {
    object = { can-be-nested: true }
    tuple = [3, "cm"]
  }
}

// Collection types may specify a type, but can also be "any".
variable "list" {
  type: list(string)
  default = ["red", "green", "blue"]
}

variable "map" {
  type: map(any)
  default = {
    red = "#FF0000"
    "green" = "#00FF00"
  }
}

variable "favourites" {
  type: set
  default = ["red", "blue"]
}

// When the type is not specified or is a mix of scalars
// they will be converted to strings.

// Use modern IDEs for type completion features. It does not matter
// in which file and in which order you define a variable, it becomes
// accessible from anywhere.

// Default values for variables may not use expressions, but you can
// use locals for that. You don't specify types for locals. With locals
// you can create intermediate products from other variables, modules,
// and functions.

locals {
  ready = var.ready ? "yes": "no"

  yaml = yamldecode(file("${path.module}/file-in-current-folder.yaml"))
}

// 'locals' blocks can be defined multiple times, but all variables,
// resources and local names should be unique

locals {
  set = toset(var.map)
}

module "more-resources" {
  source = "../more-learning"
  yaml-data = local.yaml
}

// Modules can declare outputs, that can be optionally referenced
// (see above), typically outputs appear at the bottom of the file or
// in "outputs.tf".
output "knowledge" {
  value = "types so far, more to come"
}
```

Terraform exists for managing cloud "resources". A resource could be anything as long as it
can be created and destroyed through an API call. (compute instance, distribution,
DNS record, S3 bucket, SSL certificate or permission grant). Terraform relies on "providers"
for implementing specific vendor APIs. For example the "aws" provider enables use of resources
for managing AWS cloud resources.

When `terraform` is invoked (`terraform apply`) it will validate code, create all resources
in memory, load their existing state from a file (state file), refresh against the current
cloud APIs and then calculate the differences. Based on the differences, Terraform proposes
a "plan" - series of create, modify or delete actions to bring your infrastructrue in
alignment with an HCL definition.

Terraform will also automatically calculate dependencies between resources and will maintain
the correct create / destroy order. Failure during execution allows you to retry the entire
process, which will usually pick off where things finished.

## more-learning

Time to introduce resources.

```terraform
variable "yaml-data" {

  // config is sourced from a .yaml file, so technically it is a
  // map(any), but we can narrow down type like this:
  type = map(string)
}

// You do not need to explicitly define providers, they all have reasonable
// defaults with environment variables. Using a resource that relies on a
// provider will also transparently initialize it (when you invoke terraform init)
resource "aws_s3_bucket" "bucket" {
  bucket = "abc"
}

// You can also create provider aliases
provider "aws" {
  alias = "as-role"
  assume_role {
    role_arn = ".."
  }
}

// then use them to create resources
resource "aws_s3_bucket_object" "test-file" {

  // all resources have attributes that can be referenced. Some of those
  // will be available right away (like bucket) and others may only
  // become available after the plan begins executing. The test-file resource
  // will be created only after aws_s3_bucket.bucket finishes being created

  // depends_on = aws_s3_bucket.bucket
  bucket = aws_s3_bucket.bucket.bucket
  key = "index.html"
  content = file("${path.module}/index.html")

  // you can also manually specify provider alias
  provider = aws.as-role
}

// Each resource will receive an ID in state, like "aws_s3_bucket.bucket".
// When resources are created inside a module, their state ID is prepended
// with module.<module-name>

module "learn-each" {
  source = "../learn-each"
}

// Nesting modules like this may not be the best practice, and it's only
// used here for illustration purposes
```

## learn-each

Terraform offers some great features for creating series of objects:

```terraform
locals {
  list = ["red", "green", "blue"]
}
resource "aws_s3_bucket" "badly-coloured-bucket" {
  count = count(local.list)
  bucket_prefix = "${local.list[count.index]}-"
}
// will create 3 buckets, prefixed with "red-", etc. and followed by
// a unique identifier. Some resources will automatically generate
// a random name if not specified. The actual name of the resource
// (or bucket in this example) can be referenced as attributes

output "red-bucket-name" {
  value = aws_s3_bucket.badly-coloured-bucket[0].bucket
}

// note that bucket resource ID will be "aws_s3_bucket.badly-coloured-bucket[0]"
// through to 2, because they are list index elements. If you remove "red" from
// the list, however, it will re-create all the buckets as they would now
// have new IDs. A better way is to use for_each

resource "aws_s3_bucket" "coloured-bucket" {
  // for_each only supports maps and sets
  for_each = toset(local.list)
  bucket_prefix = "${each.value}-"
}

// the name for this resource would be aws_s3_bucket.coloured-bucket[red]

output "red-bucket-name2" {
  value = aws_s3_bucket.badly-coloured-bucket["red"].bucket
}

output "all-bucket-names" {

  // returns a list containing bucket names - using a "splat expression"
  value = aws_s3_bucket.coloured-bucket[*].bucket
}

// there are other splat expressions:
output "all-bucket-names2" {
  value = [for b in aws_s3_bucket.coloured-bucket: b.bucket]
}
// can also include a filter
output "filtered-bucket-names" {
  value = [for b in aws_s3_bucket.coloured-bucket:
    b.bucket if length(b.bucket) < 10 ]
}

// here are some ways to generate maps {red: "red-123123.."}
output "bucket-map" {
  value = {
    for b in aws_s3_bucket.coloured-bucket:
       trimsuffix(b.bucket_prefix, '-')
         => b.bucket
   }
}

// as of Terraform 0.13 it is now also possible to use count/each for modules

variable "learn-functions" {
  type = bool
  default = true
}

module "learn-functions" {
  count = var.learn-functions ? 1: 0
  source = "../learn-functions"
}
```

This is now popular syntax that works in Terraform 0.13 that allows including modules conditionally.

## learn-functions

Terraform does not allow you to define your own functions, but there's an extensive list of built-in functions

```terraform
locals {
  list = ["one", "two", "three"]

  upper_list = [for x in local.list : upper(x) ] // "ONE", "TWO", "THREE"

  map = {for x in local.list : x => upper(x) } // "one":"ONE", "two":"TWO", "three":"THREE"

  filtered_list = [for k, v in local.map : substr(v, 0, 2) if k != "two" } // "ON", "TH"

  prefixed_list = [for v in local.filtered_list : "pre-${k}" } // "pre-ON", "pre-TH"

  joined_list = join(local.upper_list,local. filtered_list) // "ONE", "TWO", "THREE", "pre-ON", "pre-TH"

  // Set is very similar to List, but element order is irrelevant
  joined_set = toset(local.joined_list) // "ONE", "TWO", "THREE", "pre-ON", "pre-TH"

  map_again = map(slice(local.joined_list, 0, 4)) // "ONE":"TWO", "THREE":"pre-ON"
}

// Usually list manipulation can be useful either for a resource with for_each or
// to specify a dynamic block for a resource. This creates a bucket with some tags:

resource "aws_s3_bucket" "bucket" {
  name = "test-bucket"
  tags = local.map_again
}

// this is identical to:
// resource "aws_s3_bucket" "bucket" {
//   name = "test-bucket"
//   tags = {
//     ONE = "TWO"
//     THREE = "pre-ON"
//   }
// }

// Some resources also contain dynamic blocks. The next example uses a "data" block
// to look up 3 buckets (red, green and blue), then creates a policy that contains
// read-only access to the red and green buckets and full access to the blue bucket.

locals {
  buckets = {
    red = "read-only"
    green = "read-only"
    blue = "full"
  }
  // we could load buckets from a file:
  // bucket = file('bucket.json')

  actions = {
    "read-only" = ["s3:GetObject", "s3:GetObjectVersion"],
    "full" = ["s3:GetObject", "s3:GetObjectVersion", "s3:PutObject", "s3:PutObjectVersion"]
  }
  // we will look up actions, so that we don't have to repeat actions
}

// use a function to convert map keys into set
data "aws_s3_bucket" "bucket" {
  for_each = toset(keys(local.buckets))
  bucket = each.value
}

// create json for our policy
data "aws_iam_policy_document" "role_policy" {
  statement {
    effect = "Allow"
    actions = [
      "ec2:*",
    ]
    resources = ["*"]
  }

  dynamic "statement" {
    for_each = local.buckets
    content {
      effect = "Allow"
      actions = lookup(local.actions, statement.value, null)
      resources = [data.aws_s3_bucket.bucket[statement.key]]
    }
  }
}

// and this actually creates the AWS policy with permissions to all buckets
resource "aws_iam_policy" "policy" {
  policy = data.aws_iam_policy_document.role_policy.json
}
```

## Additional Resources

- [Terraform tips & tricks](https://blog.gruntwork.io/terraform-tips-tricks-loops-if-statements-and-gotchas-f739bbae55f9)
- [Building Dynamic Outputs with Terraform Expressions and Functions](https://www.thegreatcodeadventure.com/building-dynamic-outputs-with-terraform-for_each-for-and-zipmap/)