Terraform gyorstalpaló – Azure CI/CD pipeline építése GitHub Actions segítségével
Inspiráció
A cikk ötlete egy LinkedIn-posztból jött, amit Turbók Marcell osztott meg. A posztban egy egyszerű, de nagyon jól átlátható példát mutatott be: hogyan lehet egy alkalmazást Terraform segítségével Azure infrastruktúrára deployolni, miközben az egész folyamatot GitHub Actions CI/CD pipeline automatizálja.
A példa lényege az volt, hogy bemutassa, hogyan találkozik az Infrastructure as Code, a cloud infrastruktúra és a CI/CD pipeline automatizáció egy valós fejlesztési workflow-ban.
Ez a megközelítés nekem különösen tetszett, mert a cloud technológiákat nem dokumentációból lehet igazán megtanulni, hanem gyakorlati projekteken keresztül.
Ezért végigcsináltam a példát, és közben jegyzeteltem a lépéseket.
Ebben a cikkben ezt a tanuló projektet vezetem végig:
- Terraform hozza létre az Azure infrastruktúrát
- GitHub Actions futtatja a pipeline-t
- az alkalmazás automatikusan deployolódik Azure App Service-re
Mit fogunk felépíteni
Ebben a projektben egy egyszerű Node.js alkalmazást deployolunk Azure-ba. A cél az, hogy egy egyszerű git push után az egész infrastruktúra és az alkalmazás automatikusan felépüljön
A folyamat így néz ki:

Így épül fel a teljes megoldás
A projekt célja nem az volt, hogy egy teljesen új megoldást építsek, hanem hogy végigkövessem és megértsem azt a workflow-t, amelyet Marcell példája mutatott be. A gyakorlat során azt szerettem volna látni, hogyan áll össze a teljes folyamat egy valós felhős környezetben: vagyis hogyan lehet az infrastruktúrát Terraform segítségével kódban definiálni, és a telepítést egy GitHub Actions pipeline-nal automatizálni ahelyett, hogy mindent kézzel hoznánk létre az Azure Portalon.
A végső cél az volt, hogy egy egyszerű git push után automatikusan megtörténjen az azure hitelesítés, a terraform inicializálása, a környezet létrehozása vagy frissítése, majd maga az alkalmazás deploy is.
developer │ │ git push ▼ github repository │ ▼ github actions workflow │ ├─ azure login ├─ terraform init ├─ terraform plan ├─ terraform apply │ ▼ azure infrastructure │ ├─ resource group ├─ app service plan └─ linux web app │ ▼ node.js application deploy
A fenti ábra mögött valójában két külön világ dolgozik együtt. Az egyik az alkalmazás világa, ahol ott van a node.js kód. A másik az infrastruktúra világa, ahol ott van a resource group, az app service plan és a web app. A terraform az infrastruktúrát kezeli, a github actions pedig összefogja az egészet, és megfelelő sorrendben végigfuttatja a lépéseket.
Projekt struktúra
Az egész projektet egy github repository-ban tároljuk. Tudatosan külön mappába került az alkalmazás, külön mappába a terraform konfiguráció, és külön helyre a workflow fájl. Így az egész sokkal átláthatóbb lett, és rögtön látszik, hogy melyik fájl melyik feladathoz tartozik.
hello-world-app-cicd
│
├─ app
│ ├─ index.js
│ └─ package.json
│
├─ terraform
│ ├─ provider.tf
│ ├─ backend.tf
│ ├─ main.tf
│ ├─ variables.tf
│ └─ terraform.tfvars
│
└─ .github
└─ workflows
└─ deploy.yml
Röviden a struktúra logikája:
- app: itt van maga az alkalmazás, vagyis amit végül deployolok
- terraform: itt vannak az azure infrastruktúrát leíró fájlok
- .github/workflows: itt van a github actions pipeline definíciója
Ez azért fontos, mert így fejben is szét lehet választani, hogy mi az alkalmazás, mi az infrastruktúra, és mi az automatizációs logika.
Az alkalmazás mappája
Az app mappába került az egyszerű node.js tesztalkalmazás. Ennél a projektnél nem az volt a cél, hogy egy összetett webes rendszer épüljön fel, hanem hogy maga a deploy folyamat jól érthető legyen. Emiatt az alkalmazás minimális maradt.
app/index.js
const http = require('http');
const hostname = '0.0.0.0';
const port = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('hello from azure web app and github actions!');
});
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`);
});Ez a fájl az alkalmazás belépési pontja. Egy nagyon egyszerű http szervert indít, amely egy szöveges választ ad vissza. A process.env.PORT rész fontos, mert azure web app környezetben az alkalmazásnak arra a portra kell figyelnie, amit a platform biztosít számára. Ezért nem jó megoldás fixen csak egy saját portot megadni.
app/package.json
{
"name": "hello-world-app-cicd",
"version": "1.0.0",
"description": "simple node app for azure web app deployment",
"main": "index.js",
"scripts": {
"start": "node index.js"
}
}A package.json a node.js alkalmazás alapfájlja. Itt adjuk meg az alkalmazás nevét, verzióját, és ami a legfontosabb: a start scriptet. Ez mondja meg, hogyan induljon el az alkalmazás. Ha ez hiányzik vagy hibás, akkor a deploy még lehet sikeres, de a web app futás közben nem fog rendesen elindulni.
Terraform backend létrehozása
A terraform nem csak annyit csinál, hogy létrehoz néhány azure erőforrást. A háttérben folyamatosan nyilvántartja azt is, hogy mit hozott létre, minek mi az azonosítója, és hogyan kapcsolódnak egymáshoz az erőforrások. Ezt a nyilvántartást a terraform state fájl tartalmazza.
Ez a fájl általában a terraform.tfstate. Ha helyben, a saját gépemen tartanám, akkor a github actions pipeline nem tudna vele dolgozni. Ezért kellett remote backendbe tenni.
Azure-ban erre egy storage account és egy blob container jó megoldás. Nálam a backend struktúra így nézett ki:
resource group
└── storage account
└── blob container
└── terraform.tfstate
Ez a gyakorlatban azt jelenti, hogy a state fájl nem a saját gépemen van, hanem központilag az azure storage-ben. Így a github actions is ugyanahhoz a state-hez tud csatlakozni.
azure cli parancsok a backend létrehozásához
Ezeket a parancsokat a saját gépemen futtattam, miután beléptem azure cli-be.
az group create --name rg-tfstate-laci --location westeurope
az storage account create --name stlacistate67138 --resource-group rg-tfstate-laci --location westeurope --sku Standard_LRS
az storage container create --name tfstate --account-name stlacistate67138 --auth-mode loginAz első parancs létrehozta a rg-tfstate-laci nevű resource groupot. Ezt kifejezetten a terraform state tárolására használtam. A második parancs létrehozta a stlacistate67138 nevű storage accountot. A harmadik pedig ezen belül létrehozta a tfstate blob containert.
Azért jó külön resource groupba tenni a backend infrastruktúrát, mert így tisztán elválik az alkalmazás erőforrásaitól. Vagyis más helyen van a terraform saját működéséhez szükséges state tárolás, és más helyen van a létrehozandó web app környezet.
A terraform mappa és a benne lévő fájlok
A terraform mappába kerültek azok a fájlok, amelyek az azure infrastruktúrát írják le. Fontos, hogy a terraform nem különálló fájlokként kezeli ezeket, hanem egyetlen konfigurációként. Vagyis ha egy mappában több .tf fájl van, a terraform azokat együtt olvassa be.
terraform/provider.tf
provider "azurerm" {
features {}
}Ez a fájl mondja meg a terraformnak, hogy az azurerm providert használja, vagyis azure-ral fog dolgozni. Egyszerűen fogalmazva: ezzel mondom azt a terraformnak, hogy az azure resource manager api-n keresztül szeretném létrehozni és kezelni az erőforrásokat.
terraform/backend.tf
terraform {
backend "azurerm" {}
}Ez a fájl azt jelzi, hogy a terraform state-et az azurerm backend fogja tárolni. A konkrét backend paramétereket nem ebbe a fájlba írtam fixen, hanem a pipeline adja át a terraform init futtatásakor. Ez azért praktikus, mert így a backend adatok külön kezelhetők.
terraform/main.tf
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}
resource "azurerm_service_plan" "plan" {
name = var.app_service_plan_name
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
os_type = "Linux"
sku_name = "F1"
}
resource "azurerm_linux_web_app" "app" {
name = var.web_app_name
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
service_plan_id = azurerm_service_plan.plan.id
site_config {
application_stack {
node_version = "20-lts"
}
}
}Ez a fő terraform fájl, mert itt vannak leírva az azure erőforrások.
Az első resource a resource group. Ez fogja összefogni az alkalmazás infrastruktúráját azure-ban.
A második resource az app service plan. Ez az a compute réteg, amelyen a web app futni fog. Itt állítottam be, hogy linux alapú legyen, és a demó kedvéért az ingyenes F1 sku-t használja.
A harmadik resource maga az azure linux web app. Ez a platformszolgáltatás, ahová az alkalmazás deployolódik. Ebben már az is be van állítva, hogy node.js 20-as runtime-ot használjon.
Fontos, hogy a web app neve később a deploy workflow-ban is megjelenik. Vagyis ha itt hello-laci-48291 szerepel, akkor a github actions deploy lépésében is ugyanezt a nevet kell használni.
terraform/variables.tf
variable "resource_group_name" {}
variable "location" {}
variable "web_app_name" {}
variable "app_service_plan_name" {}Ez a fájl definiálja a változókat. Itt még csak azt mondom meg, hogy milyen paramétereket fog használni a konfiguráció. Az értékeket még nem itt adom meg.
terraform/terraform.tfvars
resource_group_name = "rg-laci-cicd"
location = "westeurope"
web_app_name = "hello-laci-48291"
app_service_plan_name = "webapp-asplan-laci"Ebben a fájlban adtam meg a konkrét értékeket. Vagyis itt döntöttem el, hogy mi legyen a resource group neve, melyik régióban jöjjön létre a környezet, mi legyen a web app neve és mi legyen az app service plan neve.
Ez a szétválasztás azért jó, mert ugyanaz a terraform kód később más értékekkel is használható. Például ha másik web app nevet, másik régiót vagy másik resource groupot szeretnék használni, akkor nem a main.tf-et kell átírni, hanem elég a változóértékeket módosítani.
Hogyan léptem be azure cli-be?
Mielőtt a backend infrastruktúrát vagy a service principalt létrehoztam volna, először be kellett jelentkeznem azure cli-be a saját gépemen.
az loginEzzel a paranccsal a cli megnyitotta a böngészős bejelentkezést, ott hitelesítettem magam, és utána már tudtam azure cli parancsokat futtatni. Ez különösen fontos lépés volt, mert enélkül a resource group létrehozás, a storage account létrehozás és a service principal készítés sem működik.
Amikor több előfizetés is elérhető, érdemes ellenőrizni, hogy tényleg a megfelelő subscription az aktív:
az account show
az account list --output tableHa nem a megfelelő subscription volt aktív, akkor át kellett állítani:
az account set --subscription "<SUBSCRIPTION_NAME_VAGY_ID>"azure service principal létrehozása
A github actions workflow-nak hitelesítenie kell magát azure felé. Ehhez nem lehet egy sima felhasználói fiókot használni, mert a pipeline nem emberként fut. Erre való a service principal, amely tulajdonképpen egy gépi identitás azure-ban.
az ad sp create-for-rbac --name github-actions-laci --role contributor --scopes /subscriptions/<SUBSCRIPTION_ID>Ezt a parancsot szintén a saját gépemen futtattam. A parancs létrehozott egy service principalt, és hozzárendelte a contributor szerepkört az adott subscription szintjén. Ez azt jelenti, hogy a pipeline képes lesz erőforrásokat létrehozni és módosítani azure-ban.
A parancs futása után megjelentek a legfontosabb adatok, például a client id, a client secret, a tenant id és a subscription id. Ezeket később a github secrets-be kellett betennem.
Itt egy fontos tanulság is volt: ha a client secret egyszer kompromittálódik vagy nyilvános helyre kerül, akkor azonnal rotálni kell. Ezt külön paranccsal lehet megtenni, új titkos kulcsot generálva a meglévő apphoz.
github secrets beállítása
A pipeline-nak szüksége van érzékeny adatokra, de ezeket nem szabad sima szövegként beleírni a repository-ba. Erre valók a github secrets.
A beállítás helye:
repository → settings → secrets and variables → actionsItt vettem fel a következő értékeket:
| secret neve | szerepe |
|---|---|
| sub_id | az azure subscription azonosítója |
| client_id | a service principal client id értéke |
| client_secret | a service principal titkos kulcsa |
| tenant_id | az azure tenant azonosítója |
A workflow-ban később ezekre a nevekre hivatkoztam. Vagyis ha a yaml fájlban az szerepel, hogy ${{ secrets.client_id }}, akkor a github ebből fogja beilleszteni a megfelelő értéket.
Ez a rész nagyon fontos, mert ha a secret neve és a workflow-ban használt hivatkozás nem egyezik, akkor a workflow nem tud helyesen belépni azure-ba.

A github actions workflow
A pipeline leírása a .github/workflows/deploy.yml fájlba került. Ez a fájl mondja meg a githubnak, hogy mikor fusson a workflow, milyen jobok legyenek benne, és milyen sorrendben hajtsa végre őket.
.github/workflows/deploy.yml
name: deploy to azure
on:
push:
branches:
- main
- laci-fix
jobs:
build:
name: build node app
runs-on: ubuntu-latest
steps:
- name: checkout code
uses: actions/checkout@v4
- name: setup node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: install dependencies
run: npm install
working-directory: app
terraform:
name: terraform infrastructure
runs-on: ubuntu-latest
needs: build
steps:
- name: checkout code
uses: actions/checkout@v4
- name: azure login
uses: azure/login@v2
with:
creds: |
{
"clientId": "${{ secrets.client_id }}",
"clientSecret": "${{ secrets.client_secret }}",
"subscriptionId": "${{ secrets.sub_id }}",
"tenantId": "${{ secrets.tenant_id }}"
}
- name: set azure credentials
run: |
echo "ARM_SUBSCRIPTION_ID=${{ secrets.sub_id }}" >> $GITHUB_ENV
echo "ARM_CLIENT_ID=${{ secrets.client_id }}" >> $GITHUB_ENV
echo "ARM_CLIENT_SECRET=${{ secrets.client_secret }}" >> $GITHUB_ENV
echo "ARM_TENANT_ID=${{ secrets.tenant_id }}" >> $GITHUB_ENV
- name: setup terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.7.0
- name: terraform init
run: terraform init -backend-config="resource_group_name=rg-tfstate-laci" -backend-config="storage_account_name=stlacistate67138" -backend-config="container_name=tfstate" -backend-config="key=terraform.tfstate"
working-directory: terraform
- name: terraform plan
run: terraform plan
working-directory: terraform
- name: terraform apply
run: terraform apply -auto-approve
working-directory: terraform
deploy:
name: deploy to azure web app
runs-on: ubuntu-latest
needs: terraform
steps:
- name: checkout code
uses: actions/checkout@v4
- name: azure login
uses: azure/login@v2
with:
creds: |
{
"clientId": "${{ secrets.client_id }}",
"clientSecret": "${{ secrets.client_secret }}",
"subscriptionId": "${{ secrets.sub_id }}",
"tenantId": "${{ secrets.tenant_id }}"
}
- name: setup node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: install dependencies
run: npm install
working-directory: app
- name: deploy to azure web app
uses: azure/webapps-deploy@v2
with:
app-name: hello-laci-48291
package: appEz a workflow három részre bomlik.
Az első a build job. Ez letölti a repository tartalmát, beállítja a node.js futtatókörnyezetet, és telepíti az alkalmazás függőségeit.
A második a terraform job. Itt történik az azure login, a terraform inicializálása, a környezet ellenőrzése és létrehozása vagy frissítése. Itt kapcsolódik a terraform a remote backendhez is.
A harmadik a deploy job. Ez már csak akkor indul el, ha a terraform job sikeresen lefutott. Itt történik maga az alkalmazás feltöltése a kész web appra.
A needs kulcsszó szabályozza ezt a sorrendet. Ez mondja meg, hogy például a deploy job csak a terraform után fusson le.
Mit csinál a terraform init, a terraform plan és a terraform apply
A terraform init az inicializálási lépés. Ilyenkor a terraform letölti a szükséges provider csomagokat, és csatlakozik a backendhez. A mi esetünkben ez az azure storage-ben tárolt state fájlhoz való kapcsolódást is jelenti.
A terraform plan egy előnézet. Megmutatja, hogy milyen erőforrásokat fog létrehozni, módosítani vagy törölni a terraform. Ez hasznos ellenőrzési pont, mert még a tényleges futtatás előtt látható, mi fog történni.
A terraform apply hajtja végre ténylegesen a módosításokat. Ekkor jön létre vagy frissül a resource group, az app service plan és az azure web app.


Hogyan klónoztam és pusholtam fel a projektet az új github profilomra?
A projektet egy új github profil alatt akartam újra használni, ezért a repository-t át kellett mozgatni a saját accountom alá. A gyakorlatban ez azt jelentette, hogy a meglévő lokális projektmappából dolgoztam tovább, de a remote már az új github repository-ra mutatott.
Először ellenőriztem, hogy melyik branchen dolgozom:
git branchNálam ez azért volt fontos, mert a módosítások a laci-fix branchben voltak, miközben a workflow eleinte csak a main branchre figyelt. Később ezért a yaml fájlban is engedélyeztem a laci-fix branch triggerelését.
Amikor a github remote már jó helyre mutatott, a szokásos módon commitoltam és pusholtam a módosításokat:
git add .
git commit -m "fix workflow azure login"
git pushHa nem volt tényleges fájlmódosítás, de a workflow-t újra akartam indítani, akkor üres commitot is lehetett használni:
git commit --allow-empty -m "trigger workflow"
git pushEz különösen hasznos volt a hibakeresés során, amikor csak azt akartam megnézni, hogy a github actions újra lefut-e a friss beállításokkal.
Milyen parancsokat futtattam hibakeresés közben?
Egy ilyen projekt összeállításakor a hibakeresés legalább annyira fontos része a tanulásnak, mint maga a működő végállapot. Néhány parancsot többször is használtam, mert ezekkel gyorsan ki lehet deríteni, hogy hol van a probléma.
Lokálisan a git állapot ellenőrzéséhez például ezeket:
git status
git branch
git remote -vAzure oldalon sokat segítettek ezek a lekérdezések:
az account show
az group list --output table
az storage account list --resource-group rg-tfstate-laci --output table
az storage container list --account-name stlacistate67138 --auth-mode login --output table
az webapp list --output tableEzekkel tudtam ellenőrizni például azt, hogy tényleg létezik-e a backend resource group, jó-e a storage account neve, megvan-e a tfstate container, illetve hogy a terraform által létrehozott web app pontosan milyen néven jött létre.
Ez utóbbi különösen fontos volt, mert a deploy job csak akkor működött, amikor a workflow-ban szereplő app-name mező pontosan megegyezett az azure-ban ténylegesen létrehozott web app nevével.
Mi történik egy git push után?
Amikor a repository-ba pusholtam a változásokat, a háttérben a következő folyamat zajlott le:
1. a github észlelte az új push-t 2. elindította a workflow-t 3. a workflow belépett azure-ba a service principal segítségével 4. a terraform csatlakozott a remote backendhez 5. a terraform létrehozta vagy frissítette a resource groupot, az app service plant és a web appot 6. a deploy job feltöltötte a node.js alkalmazást a kész web appba
Vagyis egyetlen push után nem csak az alkalmazás frissült, hanem maga a környezet is. Ez az egyik legfontosabb cloud-os szemlélet: az infrastruktúra ugyanúgy kód, mint az alkalmazás.
Milyen hibákba futottam bele a gyakorlatban?
A legelső hiba az volt, hogy a workflow csak a main branchre figyelt, én viszont a laci-fix branchen dolgoztam. Emiatt hiába pusholtam, a pipeline nem indult el. Ezt a yaml fájl trigger részének bővítésével oldottam meg.
Egy másik tipikus hiba az volt, hogy a terraform backend eleinte rossz resource group vagy storage account névre mutatott. Ilyenkor már a terraform init elbukik, mert nem találja a state tárolásához szükséges azure erőforrásokat.
Probléma volt az azure hitelesítésnél is. Önmagában az, hogy a terraformnak megadjuk az ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_TENANT_ID és ARM_SUBSCRIPTION_ID környezeti változókat, nem mindig elég. A backend eléréséhez az is kellett, hogy a workflow ténylegesen bejelentkezzen azure-ba az azure/login action segítségével.
Később a deploy is amiatt bukott meg, mert a workflow rossz web app névre hivatkozott. A terraform létrehozta a web appot, de a deploy job egy másik nevet keresett. Ezért kellett cli-ből lekérdezni, hogy pontosan mi lett a tényleges web app név, és utána a yaml fájlban azt használni.
Az ilyen hibák miatt vált igazán hasznossá a projekt, mert nem csak a “boldog útvonalat” láttam, hanem azt is, hogy a valóságban mire kell figyelni.
A végeredmény
Amikor minden a helyére került, a pipeline sikeresen lefutott, az infrastruktúra létrejött, és az alkalmazás elérhető lett a következő címen:

https://hello-laci-48291.azurewebsites.netEz volt az a pont, ahol már ténylegesen látszott a projekt értelme: egy egyszerű lokális git műveletből működő azure infrastruktúra és működő webes alkalmazás lett.
Zárás és következő lépések
Ez a projekt első ránézésre egyszerű demónak tűnhet, de valójában rengeteg fontos felhős alapelvet megmutat egyszerre. Meg lehet belőle érteni, hogyan működik a terraform, mire való a remote backend, hogyan hitelesít egy pipeline azure felé, hogyan kapcsolódik össze az infrastruktúra és az alkalmazás deploy, és hogyan lesz egy sima git push-ból valódi cloud automatizáció.
Innen több irányba is tovább lehet menni. A következő logikus fejlesztések például ezek:
- terraform modulok használata
- külön dev, test és production környezetek
- pull request alapú terraform plan ellenőrzés
- azure key vault integráció
- oidc alapú github–azure hitelesítés
- application settings és connection string kezelés
- blue-green vagy canary deployment
Ezzel a mintával már nem csak egy demót lehet építeni, hanem egy valódi, továbbfejleszthető cloud alapot is.