I denne øvelsen skal du bygge en gjenbrukbar Terraform-modul som hoster en statisk nettside på AWS. Du lærer å organisere Terraform-kode i moduler, dele state i S3, legge CloudFront foran bucketen og automatisere deployment med GitHub Actions.
- Terraform-moduler: Pakke infrastruktur i gjenbrukbare komponenter
- Remote state: Dele Terraform state i S3 og låse den for samtidige endringer
- CloudFront CDN: Global distribusjon med HTTPS foran S3
- CI/CD med GitHub Actions: Automatisere
terraform planogapply
- S3 (Simple Storage Service): Objektlager. Hoster de statiske filene som utgjør nettsiden, og lagrer Terraform state remote.
- CloudFront: AWS sitt CDN. Distribuerer nettsiden globalt og legger HTTPS på toppen av S3.
- IAM: Identity and Access Management. Styrer gjennom bucket policies og GitHub Actions-credentials hvem som kan lese og endre hva.
En fork er din egen kopi av et GitHub-repo under din GitHub-konto. Du jobber i din kopi uten å påvirke originalen, og kan senere åpne pull requests tilbake hvis du vil bidra endringer. I denne labben trenger du en fork for å kunne pushe commits (Del 4 krever det) og legge inn repository secrets i ditt eget repo.
- Fork dette repositoriet til din egen GitHub-konto
- Åpne Codespace: Klikk på "Code" → "Codespaces" → "Create codespace on main"
- Vent på at Codespace starter: Dette kan ta et par minutter første gang
Terraform og AWS CLI trenger AWS-nøkler for å kunne snakke med AWS-kontoen din. En Codespace starter uten disse.
Hent Access Key ID og Secret Access Key fra AWS Academy / IAM, og kjør:
aws configureFyll inn verdiene:
- AWS Access Key ID: fra kontoen din
- AWS Secret Access Key: fra kontoen din
- Default region name:
eu-west-1 - Default output format:
json
Hvis du bruker AWS Academy må du i tillegg sette AWS_SESSION_TOKEN. Verifiser at nøklene fungerer:
aws sts get-caller-identityKommandoen skal returnere konto-ID og bruker-ARN.
Når flere personer jobber med samme infrastruktur, eller når vi skal automatisere med CI/CD, trenger vi en felles plass å lagre Terraform state. Lokal state fungerer ikke i team-miljøer.
Lag providers.tf i rotmappen:
terraform {
required_version = ">= 1.10"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "eu-west-1"
}Klassen har en felles S3 bucket for Terraform state: pgr301-terraform-state i eu-west-1. Du trenger ikke opprette din egen — bruk denne, men gi din state en unik key slik at du ikke overskriver medstudenter.
Opprett backend.tf i rotmappen:
terraform {
backend "s3" {
bucket = "pgr301-terraform-state"
key = "ola-nordmann/website/terraform.tfstate" # Bytt "ola-nordmann" til ditt eget navn
region = "eu-west-1"
use_lockfile = true
encrypt = true
}
}key er stien til din state-fil inne i bucketen. Prefikset (f.eks. ola-nordmann/) skiller din state fra andre studenters. use_lockfile = true ber Terraform låse state via en lås-fil i selve S3-bucketen (støttet fra Terraform 1.10).
terraform initDette validerer backend-konfigurasjonen og laster ned AWS-provideren. State-filen i S3 blir først opprettet når du kjører terraform apply i Del 2.
Når du har gjort første terraform apply i Del 2 og det ligger en state-fil i S3, kan du teste state locking:
- Terminal 1: Kjør
terraform applyog bekreft medyes - Terminal 2: Kjør raskt
terraform applymens Terminal 1 fortsatt jobber
Vær rask: terraform apply fullføres fort når det ikke er mange endringer.
Terminal 2 får en feilmelding om at state er låst, med informasjon om hvem som holder låsen. Dette forhindrer at to personer gjør motstridende endringer samtidig.
Moduler er Terraforms måte å pakke og gjenbruke infrastruktur-kode på. I stedet for å duplisere kode, lager vi en modul som kan brukes flere steder med ulike konfigurasjoner. Strukturelt ligner en modul på en funksjon i et programmeringsspråk: variablene er input, ressursene er logikken, og outputs er returverdiene.
En typisk Terraform-modul består av følgende filer:
modules/s3-website/
├── main.tf # Hovedressurser (S3, CloudFront, etc.)
├── variables.tf # Input-variabler som modulen tar imot
└── outputs.tf # Output-verdier som modulen returnerer
Fordeler med moduler:
- Gjenbrukbarhet: Samme modul kan brukes i flere prosjekter eller miljøer
- Abstrahering: Skjuler kompleksitet bak et enkelt grensesnitt
- Standardisering: Sikrer konsistent infrastruktur på tvers av prosjekter
- Vedlikehold: Endringer på ett sted propagerer til alle bruksområder
Lag mappestrukturen for modulen:
mkdir -p modules/s3-website
touch modules/s3-website/main.tf
touch modules/s3-website/variables.tf
touch modules/s3-website/outputs.tfmkdir -poppretter mappen og eventuelle manglende mellomliggende mappertouchoppretter tomme filer som fylles inn i de neste stegene
Variabler gjør modulen gjenbrukbar — samme modul kan brukes med ulike verdier for forskjellige miljøer.
Fyll inn modules/s3-website/variables.tf:
variable "bucket_name" {
description = "Name of the S3 bucket"
type = string
}
variable "tags" {
description = "Tags to apply to resources"
type = map(string)
default = {}
}Fyll inn modules/s3-website/main.tf med S3-ressursene for en statisk nettside:
resource "aws_s3_bucket" "website" {
bucket = var.bucket_name
tags = var.tags
}
resource "aws_s3_bucket_website_configuration" "website" {
bucket = aws_s3_bucket.website.id
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
resource "aws_s3_bucket_public_access_block" "website" {
bucket = aws_s3_bucket.website.id
block_public_acls = false
block_public_policy = false
ignore_public_acls = false
restrict_public_buckets = false
}
resource "aws_s3_bucket_policy" "website" {
bucket = aws_s3_bucket.website.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "PublicReadGetObject"
Effect = "Allow"
Principal = "*"
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.website.arn}/*"
}
]
})
depends_on = [aws_s3_bucket_public_access_block.website]
}Fyll inn modules/s3-website/outputs.tf:
data "aws_region" "current" {}
output "bucket_name" {
description = "Name of the S3 bucket"
value = aws_s3_bucket.website.id
}
output "website_url" {
description = "URL of the S3 website"
value = "http://${aws_s3_bucket.website.bucket}.s3-website.${data.aws_region.current.name}.amazonaws.com"
}
output "bucket_arn" {
description = "ARN of the S3 bucket"
value = aws_s3_bucket.website.arn
}Opprett main.tf i rotmappen (ikke inne i modulen) som kaller modulen:
module "s3_website" {
source = "./modules/s3-website"
bucket_name = "ola-nordmann-pgr301-website" # Bytt til noe globalt unikt (f.eks. ditt-navn-pgr301-website)
tags = {
Name = "PGR301 Lab"
Environment = "Demo"
ManagedBy = "Terraform"
}
}
output "s3_website_url" {
value = module.s3_website.website_url
description = "URL for the S3 hosted website"
}
output "bucket_name" {
value = module.s3_website.bucket_name
description = "Name of the S3 bucket"
}Viktig om bucket-navn: S3 bucket-navn må være globalt unike på tvers av alle AWS-kontoer. Bruk noe som dine initialer eller studentnummer kombinert med pgr301-website.
terraform init # Re-init siden du har lagt til en modul
terraform plan
terraform applyNår apply er ferdig, skal du se s3_website_url som output. Verifiser også at state-filen nå ligger i S3-bucketen pgr301-terraform-state under din key.
Repoet inneholder en enkel statisk nettside i website/-mappen. Last opp filene til din bucket:
aws s3 sync website/ s3://ola-nordmann-pgr301-websiteaws s3 synckopierer filer og speiler kataloginnhold- Bytt bucket-navnet til ditt eget
Hent URL-en og åpne den i nettleseren:
terraform output s3_website_urlLegg til en enable_versioning-variabel i modulen som gjør versioning valgfri:
resource "aws_s3_bucket_versioning" "website" {
count = var.enable_versioning ? 1 : 0
bucket = aws_s3_bucket.website.id
# ...
}Hint: Bruk count eller for_each basert på variabelen.
S3 website hosting har begrensninger:
- Ingen HTTPS-støtte
- Ikke globalt distribuert (treg for brukere langt fra bucket-regionen)
CloudFront løser disse problemene.
Utvid modules/s3-website/main.tf med CloudFront:
resource "aws_cloudfront_distribution" "website" {
enabled = true
default_root_object = "index.html"
origin {
domain_name = aws_s3_bucket_website_configuration.website.website_endpoint
origin_id = "S3-${var.bucket_name}"
custom_origin_config {
origin_protocol_policy = "http-only"
http_port = 80
https_port = 443
origin_ssl_protocols = ["TLSv1.2"]
}
}
default_cache_behavior {
target_origin_id = "S3-${var.bucket_name}"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
min_ttl = 0
default_ttl = 0 # Instant refresh - ingen caching
max_ttl = 0
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}Utvid modules/s3-website/outputs.tf:
output "cloudfront_url" {
description = "CloudFront distribution URL (HTTPS enabled)"
value = "https://${aws_cloudfront_distribution.website.domain_name}"
}
output "cloudfront_domain" {
description = "CloudFront domain name"
value = aws_cloudfront_distribution.website.domain_name
}I rot-main.tf, legg til CloudFront output:
output "cloudfront_url" {
value = module.s3_website.cloudfront_url
description = "CloudFront URL with HTTPS"
}terraform applyCloudFront-deployment tar typisk 3-15 minutter.
terraform output cloudfront_urlÅpne URL-en i nettleseren. HTTPS fungerer automatisk, og URL-en er global (CloudFront, ikke region-spesifikk).
Automatiser Terraform deployment:
- Pull Request: Kjør
terraform planog vis endringer i PR - Merge til main: Kjør
terraform applyautomatisk
Lag .github/workflows/terraform.yml:
name: Terraform Infrastructure
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
AWS_REGION: eu-west-1
TF_VERSION: 1.10.0
jobs:
terraform:
name: Terraform Plan & Apply
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
run: terraform init
- name: Terraform Format Check
run: terraform fmt -check
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -no-color # -no-color gir pen output i PR-kommentar
continue-on-error: true
- name: Comment Plan on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const output = `### Terraform Plan
\`\`\`
${{ steps.plan.outputs.stdout }}
\`\`\`
*Pushed by: @${{ github.actor }}*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve # -auto-approve hopper over interaktiv bekreftelseGitHub Actions ser kun workflows som ligger i repoet på GitHub. Commit og push workflow-filen før du går videre — ellers trigges ingenting:
git add .github/workflows/terraform.yml
git commit -m "Add Terraform CI/CD workflow"
git pushGi GitHub Actions tilgang til AWS:
- Gå til ditt GitHub repository
- Settings → Secrets and variables → Actions
- Klikk New repository secret og legg til to secrets med dine egne verdier:
AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY
Disse secrets bør være fra en dedicated IAM-bruker med minimal permissions (kun det Terraform trenger).
- Lag en ny branch:
git checkout -b test-pipeline- Gjør en liten endring i rot-
main.tf— legg tilPipelineTest = "true"i tags-blokken:
module "s3_website" {
# ...
tags = {
# ...
PipelineTest = "true"
}
}Workflowen kjører terraform fmt -check. Kjør terraform fmt lokalt først — den formatterer koden korrekt så pipelinen ikke feiler:
terraform fmt- Commit og push:
git add .
git commit -m "Test GitHub Actions pipeline"
git push origin test-pipeline- Opprett Pull Request på GitHub
- GitHub Actions kjører
terraform plan, og plan-outputen vises som kommentar på PR-en - Merge PR til main — da kjører
terraform applyautomatisk
Du har nå lært:
- Remote State Management: State-deling i team og CI/CD
- Terraform-moduler: Gjenbrukbar infrastruktur-kode
- CloudFront CDN: Global distribusjon med HTTPS
- CI/CD med GitHub Actions: Automatisert
terraform planogapply
Når du er ferdig med labben, rydd opp etter deg for å unngå løpende AWS-kostnader og søppel i den delte state-bucketen.
- Tøm nettside-bucketen og destroy infrastrukturen:
aws s3 rm s3://ola-nordmann-pgr301-website --recursive # Bytt til ditt bucket-navn
terraform destroy- Slett din state-fil fra den delte bucketen:
aws s3 rm s3://pgr301-terraform-state/ola-nordmann/website/terraform.tfstate # Bytt prefikset til ditt egetLegg til validation i modules/s3-website/variables.tf slik at modulen avviser ugyldige bucket-navn før terraform apply:
variable "bucket_name" {
description = "Name of the S3 bucket"
type = string
validation {
condition = can(regex("^[a-z0-9][a-z0-9-]*[a-z0-9]$", var.bucket_name))
error_message = "Bucket name must start and end with lowercase letter or number, and contain only lowercase letters, numbers, and hyphens."
}
}Test ved å prøve en ugyldig verdi (f.eks. Bucket_Med_Store_Bokstaver) og se at terraform plan feiler med din feilmelding.