parallelism 파라미터
Terraform CLI는 --parallelism=N 옵션을 통해 동시에 실행할 리소스의 최대 개수를 제한할 수 있습니다. 기본값은 10입니다.
internal/terraform/context.go 테라폼 코드 바로가기
par := opts.Parallelism
// 음수는 에러 반환
if par < 0 {
return nil, diags
}
// 0이면 기본값 10 적용
if par == 0 {
par = 10
}
parallelism 값이 0이면 기본값 10으로 설정되며, 음수 값이 입력되면 오류를 반환하며 실행을 중단합니다. 이 설정 값은 내부적으로 세마포어를 통해 병렬 실행 수를 제어하는 데 사용됩니다.
테라폼은 병렬 실행 수를 제어하기 위해 Go의 buffered channel을 기반으로 한 세마포어 구조를 사용합니다.
internal/terraform/semaphore.go
func NewSemaphore(n int) Semaphore {
if n <= 0 {
panic("semaphore with limit <=0")
}
ch := make(chan struct{}, n)
return Semaphore(ch)
}
내부적으로 chan struct{} 타입의 버퍼 채널을 사용해 세마포어를 만듭니다. 버퍼 크기 n은 병렬로 동시에 실행 가능한 최대 리소스 수를 의미합니다.
internal/terraform/graph_walk_context.go
func (w *ContextGraphWalker) Execute(ctx EvalContext, n GraphNodeExecutable) tfdiags.Diagnostics {
// Acquire a lock on the semaphore
w.Context.parallelSem.Acquire() // 세마포어 락 획득
defer w.Context.parallelSem.Release() // 종료 후 해제
return n.Execute(ctx, w.Operation)
}
테라폼은 각 리소스를 실행할 때 Acquire()를 호출하여 세마포어 자원을 획득하고, 실행이 끝나면 Release()로 반환합니다. 이로 인해 설정된 개수를 초과하는 작업은 자동으로 대기 상태에 들어가며, 최대 병렬 실행 수가 제한됩니다.
🔍 버퍼 채널이란?
Go에서는 기본적으로 채널은 하나의 값을 주고받을 때마다 송신자와 수신자가 서로 기다려야 하지만, 버퍼 채널은 미리 정해진 크기만큼 값을 쌓아둘 수 있기 때문에 기다리지 않고 정해진 개수만큼 값을 미리 보내 둘 수 있습니다.
테라폼에서는 이 특성을 이용해 동시에 실행할 수 있는 작업의 수를 조절합니다. 버퍼가 가득 차면 새로운 값은 들어갈 수 없고 대기하게 되므로, 동시에 실행되는 작업 수가 n을 넘지 않도록 보장할 수 있습니다.
테라폼은 리소스 간의 의존 관계를 DAG(방향성 비순환 그래프)로 표현하고, 이 그래프를 따라 리소스를 실행합니다. 쉽게 말해, 어떤 리소스가 다른 리소스를 참조하거나 의존할 경우, 해당 리소스가 먼저 실행된 후에 다음 리소스가 실행되도록 설계되어 있습니다.
Walker 구조체
type Walker struct {
Callback WalkFunc // 실제 리소스를 실행할 함수
vertices Set // 모든 리소스(vertex) 목록
edges Set // 의존 관계(edge) 목록
vertexMap map[Vertex]*walkerVertex // 각 리소스의 실행 상태 저장
wait sync.WaitGroup // 전체 작업 완료 대기
...
}
고루틴 생성 전략
테라폼은 각 리소스를 실행할 때 2개의 고루틴을 생성합니다.
// Walker will create V*2 goroutines (one for each vertex, and dependency
// waiter for each vertex). In general this should be of no concern unless
// there are a huge number of vertices.
이렇게 분리하면 작업 실행과 의존성 대기가 서로 간섭 없이 병렬로 진행되기 때문에 리소스 수가 많아도 각 작업이 언제 시작해야 할지를 정확하고 빠르게 판단할 수 있습니다.
실행 대기: waitDeps
func (w *Walker) waitDeps(...) {
for dep, depCh := range deps {
<-depCh // 각 의존성 리소스가 끝날 때까지 대기
}
// 모든 의존성이 성공했는지 확인
for dep := range deps {
if w.diagsMap[dep].HasErrors() {
doneCh <- false // 실패한 의존성이 있음
return
}
}
doneCh <- true // 모두 성공 → 실행 시작 가능
}
의존성이 없는 리소스는 바로 실행되고, 의존성이 있는 리소스는 상위 작업이 끝날 때까지 기다립니다. 만약 상위 작업 중 하나라도 실패하면, 해당 리소스는 실행되지 않고 건너뛰게 됩니다.
실행: walkVertex
func (w *Walker) walkVertex(v Vertex, info *walkerVertex) {
// 의존성 대기
...
// 모든 의존성이 성공했을 때만 실행
if depsSuccess {
diags = w.Callback(v) // 실제 작업 실행
} else {
diags = diags.Append(errors.New("upstream dependencies failed"))
}
// 실행 결과 저장
w.diagsMap[v] = diags
}
모든 의존 리소스가 정상적으로 실행되었을 때만 작업을 수행합니다. 실행 후에는 성공/실패 결과를 기록하고, 다음 리소스들이 이 결과를 참고할 수 있도록 합니다.