mirror of
https://github.com/adambard/learnxinyminutes-docs.git
synced 2024-12-23 09:41:36 +00:00
[hcl/en] Add guide for Terraform/HCL (#3949)
* add hcl * wip * Proofread --------- Co-authored-by: Boris Verkhovskiy <boris.verk@gmail.com>
This commit is contained in:
parent
e7271a9e1c
commit
e50493e4d3
368
hcl.html.markdown
Normal file
368
hcl.html.markdown
Normal file
@ -0,0 +1,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/)
|
Loading…
Reference in New Issue
Block a user