← all posts

AWS Terraform

November 10, 2022
AWS Terraform

Untitled

DevOps Terraform Basic 1

  • infrastructure as code?
    1. Use code to provision hardware resources (Create, Update/Change, Destroy — CUD) (Terraform)
    2. deploy/update applications on top of the infrastructure
    3. manage the configurations used by the applications (Ansible)
  • Terraform vs Ansible
    1. Terraform: responsible for the infrastructure
    2. Ansible: responsible for configuring that infrastructure
  • Terraform: core component one → input sources
    1. core: TF-configuration — what to create/configure?
    2. state: snapshot of the current infrastructure state core compares both → plan: what needs to be CUD? Modifies the current state based on the desired configuration
  • Terraform: core component two → main components
    1. providers: various clouds, Kubernetes, services
  • declarative vs imperative?
    1. declarative: "give me a cheeseburger" 🤓 Terraform
    2. imperative: step-by-step instructions on how to do it (toast the bun, grill the patty, add cheese...)
  • acl: access control list — manages permissions
  • Uses HCL (Hashicorp Configuration Language)
  • init → plan → apply → destroy

DevOps Terraform 2

1605496195438-image.png

  • VPC spans all AZs (subnets) in the region — one region has many VPCs, each AZ has a corresponding subnet
  • subnet level control ⇒ NACL
  • instance level control ⇒ security group
  • Terraform
    1. source: create new cloud resource
    2. data: get known resources already from the cloud
    3. provider == import library
    4. resource/data == function
    5. tags = {Name: XXX} — note: only when Name is capitalized does it modify the VPC's Name attribute
    6. terraform destroy -target aws_subnet.dev-subnet-2 — delete a specific resource
    7. terraform apply -auto-approve — automatically approve changes
  • Terraform state
    1. list: list all resources in the current state
    2. show <resource>
  • Terraform variables:
    1. as parameters
    2. input via command line
    3. terraform.tfvars file to record variables
    4. export TF_VAR_<variable_name>="Oldiron666"
  • Terraform multi-environment deployment?
    1. Create different Terraform.vars files, e.g. terraform-dev.tfvars
    2. terraform apply -var-file="terraform-dev.tfvars"
    3. Switch working directory/environment first: terraform workspace new uat
    4. terraform apply -var-file=uat.tfvars or terraform apply -var-file=.tfvars
    5. S3 will generate a .environment environment and automatically help organize multi-environment setups

DevOps Terraform 3

  • Do you know why Sydney has three sub-VPCs? One per AZ!
  • NACL: firewall at the subnet level, open by default
  • Security Group: restricts at the server level, closed by default
  • route table: like roads, controls traffic within a private VPC
    1. local = within the VPC
  • Both the internet gateway and AWS route table are virtual!
    1. Internet gateway == virtual modem
    2. route table == virtual router
  • The procedure to connect the network for a VPC:
    1. vpc
    2. Internet gateway
    3. route table
    4. connect route table with the internet gateway
    5. connect route table with subnet (subnet association) — so traffic in the subnet is handled by the routing table Note: if no subnet association is specified, the default subnet is assigned automatically
  • A note on SSH keys:
    1. With GitHub, you provide your public key to GitHub
    2. The key downloaded from AWS is a private key
    3. When connecting with your own SSH key, you first send the public key to AWS, then log in with your private key This is a classic asymmetric-then-symmetric encryption flow: public key for transmission, private key for decryption. SSH uses the private key by default — just run ssh <connectionString> and save time. Note: on first authentication you need to add the host to known_hosts — it will ask you to confirm with yes

DevOps Terraform final

  • Modules: like functions in programming — encapsulate code and improve reusability. A module should combine at least 3+ resources
    1. root module
    2. /modules = "child modules" called by another configuration
  • input variables == function arguments
  • output values == function return values
  • external Terraform uses source to determine the module's origin (we use source to add path for our local module)
  • configure a remote backend to safely store and share Terraform state via S3
    1. All state changes are uploaded directly to S3
    2. Terraform state list can connect remotely to S3 to view created and active resources

Then I did something dumb — I manually deleted the state file that had been uploaded to S3, which meant all the already-created resources had to be deleted manually...

  • handle version and shared storage for state
    1. S3 for shared storage for state files
    2. DynamoDB for version locking
  • Pitfalls:
    1. Terraform configuration is mainly set in the backend block — remember to run init after configuring it
    2. S3 and DynamoDB tables need to be created manually first, then referenced by name in the backend block — otherwise Terraform will try to find them from AWS and fail
    3. If you let Terraform auto-create S3 and DynamoDB, running destroy will wipe them too — a strange default behavior

Downside: Terraform still feels like an immature product, though the concept is solid. The cascading variable passing issue and the way state and lock files are stored both feel unreasonable.

DevOps Terraform Frontend Pitfalls

  • The frontend stack consists of S3, CDN, ACM, and Route53
    1. The tricky part of S3 is fetching the policy from aws_iam_policy_document — this policy only allows CDN to access S3's GetObject via OAI. It's fetched using data sources, and data source outputs cannot be returned from a module via outputs, so it must be written in the root main.tf.
    2. Route53: since my domain is in a different AWS account, I had to delegate it first, then fetch the zone ID via data source before proceeding. Route53 is also tightly coupled with CDN — the flow is S3 → CDN → Route53, so Route53 mainly pulls data from CDN using an alias. Additionally, Route53 creates a CNAME to validate the ACM certificate.
    3. CDN, while linked to Route53, also needs to validate the ACM certificate. In viewer_certificate, providing just the acm_certificate is not enough — per the docs, you must also set ssl_support_method (choose sni-only here), and using the default "TLSv1" protocol version lets you successfully select the certificate generated for your domain. This is a fairly deep pitfall.
    4. A key ACM pitfall is the creation region. To be linked with CloudFront, ACM must be created in Virginia (us-east-1). The entire frontend stack is largely region-agnostic since S3 data is distributed globally via CDN (though you can select coverage regions). If you need to create resources in multiple regions within a child module, you must declare a provider with a specific region and alias, then reference it in multi-region resources with provider = aws.<aliasName>. Note: the provider argument inside a resource block is undocumented — I discovered it works through trial and error.
    5. In the end, both my Route53 (acm_certificate_validation — yes, I put it in the Route53 module since Route53 is the one doing the validation) and ACM are created in Virginia.

DevOps Terraform Multi-Environment + ECS Theory

  • Terraform
    1. terraform fmt -recursive — format all files uniformly
    2. Deploy to different environments: terraform apply -var-file="<dev/prod>.tfvars"
    3. Manage multi-environment + multi-workspace ⇒ terraform workspace — create different workspaces for dev and prod
    4. provider "aws" { profile = "XXX" } — the profile matches credentials (access_key_id and secret_access_key) stored on your machine
    5. terraform init -backend-config="dev-backend.conf"
  • ECS Fargate
    1. task definition: describes one or more containers through attributes, combining multiple container definitions
      1. Also includes container definition: container image and container-level settings (port, registry, environment variables)
    2. cluster: fully managed by AWS
    3. service: allows running and maintaining a specified number of simultaneous instances of a task definition in an ECS cluster — defines how many tasks to run
    4. task: instantiated from a task definition

DevOps tf-backend Notes

Untitled

  • Why does an Application Load Balancer need both security groups and listeners? My current theory: the load balancer is implemented using EC2, so it has the concept of SGs to control which ports allow traffic in. The server also runs software like Nginx to handle load balancing, which requires port configuration. Additionally, you typically set up both listeners and routing rules — receiving traffic and forwarding it to a destination. AWS abstracts the software configuration and gives customers a better UI to manage it.
  • apt remove: removes the package but keeps its configuration files
  • apt purge: removes both the package and its configuration files
  • IAM role
    1. Similar to an IAM user, but unlike a user who is tied to one person, a role is intended to be assumed by a user, service, or application.
    2. A role does not have credentials such as a password or access key.
  • IAM user: 1 user → many groups. 1 group → many users
  • CloudFormation, Terraform, AWS CDK & Pulumi:
    1. CloudFormation and Terraform are both declarative — they just use YAML instead of HCL
    2. AWS CDK and Pulumi are both imperative — use any programming language to provision infrastructure
  • Pitfalls:
    1. lb's enable_deletion_protection = true will block destroy on the LB — set it to false first in AWS for it to be auto-destroyed, though the tutorial recommends against allowing auto-destroy
    2. ecr_repo looks for images in private by default — it won't find public images
    3. ECR repository names must be all lowercase
    4. Keep ECR images in private whenever possible

DevOps AWS Backend Notes

1cf0c8732975a2d7c01779df01d7a97.jpg

41d57fdd2b81731fbeeb74d5cba6125.jpg

  • Public knowledge:
    1. AWS CDK compiles down to CloudFormation
    2. VPC peering: allows resources in two subnets to communicate
    3. Least privilege model: only grant a service the ports it needs, even within the same private subnet — if one service is compromised, it can't access others
    4. each private subnet has one NAT
    5. a public subnet can share one NAT
    6. How does a security group demonstrate stateful behavior? If traffic can get in, it can get out. NACL is stateless.
    7. Why can the ELB target group port be set to anything? That port is the entry port from the ELB listener into the target group — it's the target group's ingress port, so any value works (just avoid reserved system ports)
    8. In Terraform, ECS containers can have assign_public_ip disabled — it's not useful anyway
    9. In Terraform SG ingress rules, from_port and to_port define a port range, e.g. 666–777. If both values are equal, only one port is opened.
    10. In SGs, generally only ingress ports are defined; egress is fully open, e.g. 1–65535.
    11. AWS is written in JavaScript — TypeScript is the best pairing (currently only studying Terraform, more later)
    12. What do metric_interval_upper_bound and lower_bound do in CloudWatch + Auto Scaling step scaling policy configuration? They work with CloudWatch to set upper and lower adjustment thresholds for multi-tier scaling. e.g. if CloudWatch alarm threshold is 50, and the metric delta is between 10–20, add one instance. If the delta is between 20–30, add two. The upper bound must always be greater than the lower bound.

Docker Image — Multiple Tags / ALB Deep Dive

  1. Tag the docker build with both a version tag and a latest tag:
  • docker tag <name> <image-name>:${BUILD_NUMBER}
  • docker tag <name> <image-name>:latest
  1. Access multiple services (Jenkins, SonarQube, Vault) through a load balancer.
  2. Use the ALB to route different paths to each service — no need to generate individual certificates per service, AWS handles certificate issuance.

Terraform Advance

  • for_each: iterate over a list or dict, one item at a time; use each to access values:
  1. each.value
  2. each.key
  • lookup: retrieve a value from a dict by key
  1. lookup(<map>, <key>)
  • file()
  1. e.g. public_key = file("~/.ssh/key.pub")
  • template: templatefile(path, vars) — useful for generating lambda configs, task_definitions
  1. e.g. generate 10+ clusters or ECS services from a template
;