Skip to main content

GitLab CI

ตัวอย่าง .gitlab-ci.yml สำหรับ Terraform pipeline บน GitLab

Basic Pipeline

.gitlab-ci.yml
image: hashicorp/terraform:1.9.8

variables:
TF_ROOT: ${CI_PROJECT_DIR}
TF_STATE_NAME: ${CI_PROJECT_NAME}

cache:
key: terraform
paths:
- .terraform/

before_script:
- cd ${TF_ROOT}
- terraform --version

stages:
- validate
- plan
- apply

fmt:
stage: validate
script:
- terraform fmt -check -recursive

validate:
stage: validate
script:
- terraform init -backend=false
- terraform validate

plan:
stage: plan
script:
- terraform init
- terraform plan -out=tfplan
artifacts:
paths:
- tfplan
expire_in: 1 day

apply:
stage: apply
script:
- terraform init
- terraform apply -auto-approve tfplan
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
dependencies:
- plan

GitLab Managed State (Built-in!)

GitLab มี Terraform State Backend ในตัว — ไม่ต้องตั้ง S3:

backend.tf
terraform {
backend "http" {
address = "https://gitlab.com/api/v4/projects/${PROJECT_ID}/terraform/state/${STATE_NAME}"
lock_address = "https://gitlab.com/api/v4/projects/${PROJECT_ID}/terraform/state/${STATE_NAME}/lock"
unlock_address = "https://gitlab.com/api/v4/projects/${PROJECT_ID}/terraform/state/${STATE_NAME}/lock"
username = "gitlab-ci-token"
password = "${CI_JOB_TOKEN}"
lock_method = "POST"
unlock_method = "DELETE"
retry_wait_min = 5
}
}

ใน CI:

plan:
script:
- terraform init
-backend-config="address=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}"
-backend-config="lock_address=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}/lock"
-backend-config="unlock_address=${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${TF_STATE_NAME}/lock"
-backend-config="username=gitlab-ci-token"
-backend-config="password=${CI_JOB_TOKEN}"
-backend-config="lock_method=POST"
-backend-config="unlock_method=DELETE"
-backend-config="retry_wait_min=5"
- terraform plan

State แสดงใน GitLab UI: Operate → Terraform states

ใช้ GitLab Terraform Template

GitLab มี template สำเร็จรูป:

.gitlab-ci.yml
include:
- template: Terraform/Base.gitlab-ci.yml

stages:
- validate
- test
- build
- deploy

fmt:
extends: .terraform:fmt

validate:
extends: .terraform:validate

build:
extends: .terraform:build

deploy:
extends: .terraform:deploy

OIDC Authentication

ใช้ GitLab OIDC token แทน AWS access key:

1. ตั้งค่า IAM

data "tls_certificate" "gitlab" {
url = "https://gitlab.com"
}

resource "aws_iam_openid_connect_provider" "gitlab" {
url = "https://gitlab.com"
client_id_list = ["https://gitlab.com"]
thumbprint_list = [data.tls_certificate.gitlab.certificates[0].sha1_fingerprint]
}

resource "aws_iam_role" "gitlab_ci" {
name = "GitLabCITerraform"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.gitlab.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringLike = {
"gitlab.com:sub" = "project_path:myorg/myrepo:ref_type:branch:ref:main"
}
}
}]
})
}

2. ใช้ใน CI Job

apply:
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
script:
- aws sts assume-role-with-web-identity
--role-arn $AWS_ROLE_ARN
--role-session-name gitlab-${CI_PIPELINE_ID}
--web-identity-token $GITLAB_OIDC_TOKEN

Multi-Environment

.gitlab-ci.yml
stages:
- validate
- plan
- apply

.terraform:
image: hashicorp/terraform:1.9.8
before_script:
- cd ${ENV_PATH}
- terraform init

validate:
stage: validate
parallel:
matrix:
- ENV: [dev, staging, prod]
variables:
ENV_PATH: envs/${ENV}
extends: .terraform
script:
- terraform validate

plan-dev:
stage: plan
variables:
ENV_PATH: envs/dev
extends: .terraform
script:
- terraform plan
rules:
- if: $CI_COMMIT_BRANCH == "develop"

plan-prod:
stage: plan
variables:
ENV_PATH: envs/prod
extends: .terraform
script:
- terraform plan -out=tfplan
artifacts:
paths: [envs/prod/tfplan]
rules:
- if: $CI_COMMIT_BRANCH == "main"

apply-prod:
stage: apply
variables:
ENV_PATH: envs/prod
extends: .terraform
script:
- terraform apply -auto-approve tfplan
environment:
name: production
deployment_tier: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
dependencies:
- plan-prod

Merge Request Plan Comment

plan:
stage: plan
script:
- terraform init
- terraform plan -no-color | tee plan.txt

# Post comment on MR
- |
if [ -n "$CI_MERGE_REQUEST_IID" ]; then
PLAN_OUTPUT=$(cat plan.txt)
BODY=$(jq -Rs --arg p "$PLAN_OUTPUT" '{body: "### Terraform Plan\n```\n\($p)\n```"}' <<< "")
curl --request POST \
--header "PRIVATE-TOKEN: ${GITLAB_API_TOKEN}" \
--header "Content-Type: application/json" \
--data "$BODY" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes"
fi
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"

Cache Plugins

.terraform:
cache:
key:
files:
- .terraform.lock.hcl
paths:
- .terraform/providers
variables:
TF_PLUGIN_CACHE_DIR: "${CI_PROJECT_DIR}/.terraform.d/plugin-cache"
before_script:
- mkdir -p ${TF_PLUGIN_CACHE_DIR}

Linting + Security

tflint:
image: ghcr.io/terraform-linters/tflint:v0.53.0
stage: validate
script:
- tflint --init
- tflint --recursive

checkov:
image: bridgecrew/checkov:latest
stage: validate
script:
- checkov -d . --framework terraform

trivy:
image: aquasec/trivy:latest
stage: validate
script:
- trivy config --severity HIGH,CRITICAL .

Approval & Manual Apply

apply-prod:
stage: apply
script:
- terraform apply -auto-approve tfplan
environment:
name: production
url: https://app.example.com
when: manual # require manual click
only:
- main

when: manual → click "Play" button ใน GitLab UI เพื่อ trigger apply

Pipeline Schedules (Drift Detection)

ใน GitLab UI → CI/CD → Schedules:

drift-check:
stage: validate
script:
- terraform init
- terraform plan -refresh-only -detailed-exitcode
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
allow_failure: true

ตั้ง cron 0 9 * * * → daily drift check

Best Practices

✅ DO:
- ใช้ GitLab managed state (built-in!) สำหรับ small projects
- ใช้ OIDC แทน access keys
- Manual approval gate สำหรับ prod (when: manual)
- Cache plugins ระหว่าง jobs
- Lint + security scan ใน parallel

❌ DON'T:
- ห้าม commit secrets — ใช้ GitLab CI/CD variables (masked + protected)
- ห้าม auto-apply prod
- ห้าม skip artifacts (plan file ต้อง persist)

ตัวอย่าง Complete Pipeline

.gitlab-ci.yml
image: hashicorp/terraform:1.9.8

variables:
TF_PLUGIN_CACHE_DIR: "${CI_PROJECT_DIR}/.terraform.d/plugin-cache"

cache:
key:
files: [.terraform.lock.hcl]
paths:
- .terraform/providers
- .terraform.d/plugin-cache

before_script:
- mkdir -p ${TF_PLUGIN_CACHE_DIR}

stages:
- validate
- plan
- apply

fmt:
stage: validate
script: terraform fmt -check -recursive

validate:
stage: validate
script:
- terraform init -backend=false
- terraform validate

tflint:
stage: validate
image: ghcr.io/terraform-linters/tflint:v0.53.0
script:
- tflint --init
- tflint --recursive

checkov:
stage: validate
image: bridgecrew/checkov:latest
script:
- checkov -d . --framework terraform

plan:
stage: plan
script:
- terraform init
- terraform plan -out=tfplan
artifacts:
paths: [tfplan]
expire_in: 7 days
rules:
- if: $CI_COMMIT_BRANCH == "main"
- if: $CI_PIPELINE_SOURCE == "merge_request_event"

apply:
stage: apply
script:
- terraform init
- terraform apply -auto-approve tfplan
dependencies: [plan]
environment:
name: production
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual

สรุป

  • GitLab CI ใช้ .gitlab-ci.yml
  • มี Terraform managed state built-in (no need S3!)
  • ใช้ template Terraform/Base.gitlab-ci.yml ลดงาน
  • OIDC authentication กับ AWS
  • when: manual สำหรับ approval gate
  • Pipeline Schedules สำหรับ drift detection

ต่อไป → Jenkins