# IaC <!-- Put the link to this slide here so people can follow --> slide: https://hackmd.io/@q9cU-jkbS6WVfnxVnHOzVw/HyRKCrZyj --- ## Overview * Concept * Terraform * HCL * State * Alternatives --- ## IaC - Cost, speed and risk: - The cost of infrastructure configuration relate to mitigation of repetative tasks - Automation enables speed of faster execution - Risk is mitigated trough automation --- # Terraform - 2014 - Ansible 2012, Chef 2009 - Push based - In what way the configuration is propagated - Pull - servers fetch the configuration and changed the setup on its own - Written in GO - Day 0 Provisioning --- # Terraform - One of many IaC implementation - Cloud agnostic - Modular - Stateful - Declarative - Standardized configuration - Somewhat colaborative --- ## HCL * Configuration language common in Hashicorp products * Something entirely new and different * Sysadmins do not need to learn howto golang, nodejs, ... --- ### Hashicorp Configuration Language (HCL & HCL 2) - created by resources ```hcl <BLOCK TYPE> "<BLOCK LABEL>" "<BLOCK LABEL>" { # Block body <IDENTIFIER> = <EXPRESSION> # Argument } ``` --- ## Resource ```hcl resource "aws_vpc" "main" { cidr_block = var.base_cidr_block } ``` --- ## Variable ```hcl variable "base_cidr_block" { description = "A description" default = "10.1.0.0/16" } ``` --- ## Output ```hcl output "test" { description = "A description" value = "test" } ``` --- ## State * Mapping of real world resources to your configuration --- ### State example ```hcl resource "random_password" "password" { length = 16 special = true override_special = "!#$%&*()-_=+[]{}<>:?" } output "password" { value = nonsensitive(random_password.password.result) } ``` --- ### State example ```bash terraform apply ``` --- ### State example ```bash Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # random_password.password will be created + resource "random_password" "password" { + bcrypt_hash = (sensitive value) + id = (known after apply) + length = 16 + lower = true + min_lower = 0 + min_numeric = 0 + min_special = 0 + min_upper = 0 + number = true + numeric = true + override_special = "!#$%&*()-_=+[]{}<>:?" + result = (sensitive value) + special = true + upper = true } Plan: 1 to add, 0 to change, 0 to destroy. Changes to Outputs: + password = (known after apply) Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes random_password.password: Creating... random_password.password: Creation complete after 0s [id=none] Apply complete! Resources: 1 added, 0 changed, 0 destroyed. Outputs: password = "{(-_92bg-:D8:W1C" ``` --- ## State management issues * Locking has to be used * Cooperation is very limited * Generally a larger file, 2 MB is ok * Secrets are placed as string in the file * `terraform refresh` can take minutes --- # Expressions * Expressions are used to refer to or compute values within a configuration. * An exception to the declarative approach For local debuging, open random directory and type `terraform console` --- ## Type & values * string * number * bool * list/tuples * map/object * null --- ## String and templates * JSON from map: ```hcl example = jsonencode({ a = 1 b = "hello" }) ``` --- ## Heredoc standard: ``` locals { test = <<EOT test test EOT } ``` --- ## Heredoc indented: ``` locals { test = <<-EOT test test EOT } ``` `local.test` : ``` test test ``` --- ## Interpolation & directives ```hcl "Test, ${var.test}!" ``` ```hcl "Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!" ``` --- ## Variable references ```hcl locals { test = "test" test_two = local.test } ``` Refering by tree structure with dots. --- ## Operators Standard, supports also ternary operator ```hcl locals { is_friday = formatdate("EEEE", timestamp()) == "Friday" ? true : false } output "friday" { value = local.is_friday } output "day" { value = formatdate("EEEE", timestamp()) } ``` Outputs: ``` day = "Thursday" friday = false ``` --- ## For expression ```hcl > [for s in ["a", "b", "c"] : upper(s)] [ "A", "B", "C", ] > {for s in ["a", "b", "c"] : s => upper(s)} { "a" = "A" "b" = "B" "c" = "C" } > [for s in ["a", "b", "c"] : upper(s) if s != "b"] [ "A", "C", ] ``` --- ## Splat expression ```hcl locals { a_list = [{"id": 1}] } > [for o in local.a_list : o.id] [ 1, ] # splat expression: > local.a_list[*].id [ 1, ] ``` Old and confusing --- # Resources Description of one or more object in infrastructure. ```hcl resource "aws_instance" "web" { ami = "ami-a1b2c3d4" instance_type = "t2.micro" } ``` [Arguments](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) given by provider. --- ## Resources meta-arguments * depends_on - explicit dependency * count - multiple resource instance * for_each - multple resources according to a map or set * provider - in case there are non-default providers * lifecycle - adjusting object lifecycle handling - when to destroy/create * provisioner - extra actions after reource creation --- ## Resources dependency Implicit ```hcl resource "google_compute_address" "static" { name = "jumper-ipv4-address" } resource "google_compute_instance" "jumper" { ... network_interface { network = "default" access_config { nat_ip = google_compute_address.static.address } } } ``` --- ## Resources dependency Explicit ```hcl resource "google_compute_instance" "jumper_a" { ... } resource "google_compute_instance" "jumper_b" { ... depends_on = [google_compute_instance.jumper_a] } ``` --- ## Local-only resources SSH, TLS keys, Random numbers, ... ```hcl resource "random_password" "password" { length = 16 special = true override_special = "!#$%&*()-_=+[]{}<>:?" } output "password" { value = random_password.password.result } ``` -- --- ## Count and for_each * Creating multiple resources * Creating resource once condition is met --- ### Count ```hcl resource "google_compute_instance" "server" { count = 10 name = "server_${count.index}" ... } ``` Each instance refered with index `google_compute_instance.server[0-9]` --- ### Count ```hcl resource "google_compute_instance" "server" { count = var.enable_server ? 1 : 0 name = "server_${count.index}" ... } ``` Creates a server once variable `enable_server` is set to true. --- ### for_each ```hcl resource "random_password" "password" { for_each = { user_1: { lower: false, special: true, } user_2: { lower: true, special: false, } } length = 16 special = each.value["special"] lower = each.value["lower"] override_special = "!#$%&*()-_=+[]{}<>:?" } output "passwords" { value = {for k, v in random_password.password: k => nonsensitive(v.result)} } ``` --- ### for_each ```hcl output "passwords" { value = {for k, v in random_password.password: k => nonsensitive(v.result)} } Outputs: passwords = { "user_1" = "*J4R>FKAOE0I4P-]" "user_2" = "Pi4Bkogxk0tvqpBq" } ``` --- ## Dynamic expressions ```hcl resource "aws_elastic_beanstalk_environment" "_" { ... setting { namespace = "dev" name = "test_1" value = "test_1" } setting { namespace = "dev" name = "test_2" value = "test_2" } } ``` --- ## Dynamic expressions ```hcl resource "aws_elastic_beanstalk_environment" "_" { ... dynamic "setting" { for_each = [ { namespace: "dev", name: "test_1" value: "test_1" }, { namespace: "dev", name: "test_2" value: "test_2" }, ] content { namespace = setting.value["namespace"] name = setting.value["name"] value = setting.value["value"] } } } ``` --- ## Useful for conditional blocks ```hcl resource "google_container_cluster" "primary" { ... dynamic "network_policy" { for_each = var.network_policy == null ? [] : [1] content { enabled = true provider = var.network_policy } } ... ``` --- # Modules - Recycable code - Version control - Modules registry at https://registry.terraform.io/browse/modules --- ## Modules folder layout ``` project │ main.tf │ └─── module_1 │ main.tf │ variables.tf │ outputs.tf │ ... ``` --- ## Modules example main.tf ```hcl module "servers" { source = "./module_1" servers = 5 } ``` --- ## Modules example `module_1/main.tf` ```hcl resource "digitalocean_droplet" "server" { count = var.servers image = "ubuntu-18-04-x64" name = "web-${count.index}" region = "nyc2" size = "s-1vcpu-1gb" } ``` --- ## Modules example `module_1/variables.tf` ```hcl variable "server" { default = 0 description = "Count of servers" } ``` --- # Providers * Binaries used for communication with upstream api * Devided in official, partner, community, archived * Anyone can create a provider * Each provider is versioned * Typically, version matches the API version * Providers are build with different architecture --- ## Providers ``` .terraform/providers/registry.terraform.io/cloudflare/cloudflare/3.1.0/linux_amd64$ ./terraform-provider-cloudflare_v3.1.0 This binary is a plugin. These are not meant to be executed directly. Please execute the program that consumes these plugins, which will load any plugins automatically ``` Providers are used in the code --- ## Providers `providers.tf`: ```hcl provider "kubernetes" { cluster_ca_certificate = var.gke_cluster_ca_certificate token = var.token host = "https://${var.gke_endpoint}" } ``` --- ## Providers `versions.tf`: ```hcl terraform { required_version = "1.2.9" required_providers { kubernetes = { source = "hashicorp/kubernetes" version = "~> 2.13.0" } } } ``` --- ## Providers `.terraform.lock.hcl`: ```hcl # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/kubernetes" { version = "2.13.1" constraints = "~> 2.13.0" hashes = [ "h1:cN3OwZvhtn/y3XfnGQ4hi+7oZp1gU2zVYhznRv2C7Qg=", "zh:061f6ecbbf9a3c6345b56c28ebc2966a05d8eb02f3ba56beedd66e4ea308e332", "zh:2119beeccb35bc5d1392b169f9fc748865261b45fb75fc8f57200e91658837c6", "zh:26c29083d0d84fbc2e356e3dd1db3e2dc4139e943acf7a318d3c98f954ac6bd6", "zh:2fb5823345ab05b3df74bb5c51c61072637d01b3cddffe3ad36a73b7d5b749e6", "zh:3475b4422fffaf58584c4d877f98bfeff075e4a746f13e985d2cb20adc873a6c", "zh:366b4bef49932d1d71b12849c1878c254a887962ff915f37982299c1185dd48a", "zh:589f9358e4a4bd74a83b97ccc64df455ddfa64c4c4e099aef30fa29080497a8a", "zh:7a0d75e0e4fee6cc5599ac9d5e91de563ce9ea7bd8137480c7abd09642a9e72c", "zh:a297a42aefe0650e3d9fbe55a3ee48b14bb8bb5edb7068c09512d72afc3d9ca5", "zh:b7f83a89b646542d02b733d464e45d6d0739a9dbb921305e7b8347e9fc98a149", "zh:d4c721174a598b66bd1b29c40fa7cffafe90bb58186cd7506d792a6b04161103", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } ``` --- ## Providers `.terraform.lock.hcl`: * Each hash represent different build platform * Semver is not enough --- ## Version constraints * Semver vA.B.C from https://semver.org/spec/v2.0.0.html * ~> update rightmost component, e.g. ~> 1.0.4 allows 1.0.5, but not 1.1.0 * <=, >=, =, != --- # Example project ``` project │ main.tf │ variables.tf │ outputs.tf │ versions.tf │ providers.tf │ terraform.tfvars │ .terraform │ .terraform.lock.hcl │ └─── module_1 │ main.tf │ variables.tf │ outputs.tf │ ... ``` --- ## Example project * Terraform works with current directory * not with single files * You can put all tf code in one file and it will work --- ## `main.tf` * Usually contains resources for configuration * `locals` block * A few comments --- ## `variables.tf` * Variables used for the configuration * If not filled in `terraform.tfvars` * values can be inserted from stdin --- ## `outputs.tf` * Display values usually needed for further configuration * `terraform refresh` & `terraform output` ```hcl output "passwords" { value = ... } ``` --- ## `versions.tf` ```hcl terraform { required_version = "1.2.2" required_providers { cloudflare = { source = "cloudflare/cloudflare" version = "~> 3.0" # configuration_aliases = [cloudflare.bg, cloudflare.hr, cloudflare.gr] } google = { source = "hashicorp/google" version = "4.24.0" } postgresql = { source = "cyrilgdn/postgresql" version = "1.16.0" } } } ``` --- ## `providers.tf` ```hcl provider "google" { project = var.project region = var.zone } provider "postgresql" { scheme = "gcppostgres" host = module.postgresql.instance_connection_name port = 5432 database = replace(var.project, "-", "_") username = "admin" password = random_password.development.result sslmode = "require" connect_timeout = 15 } provider "cloudflare" { api_token = data.vault_generic_secret.cloudflare_credentials_gr.data["API_TOKEN"] } ``` --- ## `terraform.tfvars` Let's have `variables.tf`: ``` variable "region" { default = "europe-west3" } ``` `terraform.tfvars` can contain: ``` region = "europe-west3" ``` --- ## `.terraform` * Contains providers, modules and local state ``` .terraform │ modules │ providers │ terraform.tfstate ``` --- ## `.terraform.lock.hcl` * Lock file `.terraform.lock.hcl`: ```hcl # This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/google" { version = "4.35.0" constraints = ">= 3.53.0, ~> 4.35.0, < 5.0.0" hashes = [ "h1:ybilPdQgRamexNcj8YpH74jIVGqiBIGaMULs3wx249g=", "zh:122318ed6ba4253fca3fd0f75462c58337f1d86c34f20f2918e3fd12b2f0a31c", "zh:15eb57c0c54002d04610c7682393407b23fde732993bf023536ff33a7c43212d", "zh:29f7833338f778c1bf68d663a22c38ff92d642bf05a001005c570be7f55b6fae", "zh:4f61d7cc0b02ae9e456b5547fe617c880a6396727ef9126c6ec60fab2ee6cc9e", "zh:6b84cc5b571e453afc604899748954677211ad7d97c1eda0ac0ca27764169f7f", "zh:7171bab4ac33a7852e8a7813d0742f5772666d132f8688507ecca209b19f5b3e", "zh:802ba225215490587cdffe25dd180c7af9f0ba8cca9cbb0d8b31612cb0a594b4", "zh:af6aee1833ba6a0dce980fd2b61183496e2c3ae030bb6350bd6e8a3b033fe291", "zh:d146ef63698b53bf9dd22313250f1eab196c2f0aa84c3a4dce5a70b73c55a363", "zh:dd4d00ec04d119c33ec63ab3f422a52404cefa9c8075ccc177949d119ceae16f", "zh:e93cefc27b2ec7af10f35abc9e7c9fc6163af8f9dd2e028719b7280497113605", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", ] } ``` --- # Alternatives * AWS CDK * Pulumi * CDK for Terraform --- ## Alternatives * No extra language, just use what ever you are use to: * Python * NodeJs * Golang * Imperative approach mixed with declaratives trends * Once code is erased, it have to be erased from to [cloud] * State is always present * Unit testing is done by the tests in the language --- ## CDK * npm install -g aws-cdk * cdk list * cdk synthetize - to cloudformation * cdk init app --language python|typescript * cdk diff - like terraform plan * cdk deploy & destroy - like terraform apply & destroy --- ## Pulumi * pulumi new * pulumi up - preview * pulumi stack output * pulumi console - UI hosted by Pulumi --- ## CDK for Terraform * Same principle * No HCL --- # Summary * IaC * State * Terraform * HCL * Alternatives
{"metaMigratedAt":"2023-06-17T07:23:34.512Z","metaMigratedFrom":"YAML","title":"Terraform","breaks":true,"description":"FIT CTU 22 zimní semestr","contributors":"[{\"id\":\"abd714fa-391b-4ba5-957e-7c559c73b357\",\"add\":17363,\"del\":3253}]"}
    418 views