์ฒซ ์ธํ”„๋ผ ๋‹ด๋‹น์ž๋กœ ์‚ด์•„๋‚จ๊ธฐ: ์‹œ์ž‘, ๊ณ„ํš, ๊ณ„์ • ๊ด€๋ฆฌ

jiyoung
  • #infra
  • #terraform
  • #iam
  • #aws
  • #postgresql
  • #google-workspace
  • #automation

๋“ค์–ด๊ฐ€๋ฉฐ

์•ˆ๋…•ํ•˜์„ธ์š”, ๋…ธ๋จธ์Šค์—์„œ ์ธํ”„๋ผ ์—”์ง€๋‹ˆ์–ด๋กœ ํ•ฉ๋ฅ˜ํ•œ ๋ฐ•์ง€์˜์ž…๋‹ˆ๋‹ค.

์šฐ๋ฆฌ ํšŒ์‚ฌ๋Š” ๊ฐœ๋ฐœ์ž ์ค‘์‹ฌ์œผ๋กœ ๋น ๋ฅด๊ฒŒ ์„ฑ์žฅํ•ด์™”์Šต๋‹ˆ๋‹ค.
์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค๊ณ , ์ถœ์‹œํ•˜๊ณ , ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ์„ ๋ฐ›๋Š” ๊ฒƒ์ด ์ตœ์šฐ์„ ์ด์—ˆ๊ณ ,
๊ทธ ๊ณผ์ •์—์„œ ์ธํ”„๋ผ๋Š” ๊ฐ์ž๊ฐ€ ํ•„์š”ํ•œ ๋งŒํผ ๋งŒ๋“ค๊ณ , ํ•„์š”ํ•  ๋•Œ ์ง์ ‘ ๊ณ ์ณค์Šต๋‹ˆ๋‹ค.

๋ˆ„๊ตฌ๋„ ์ž˜๋ชปํ•œ ์‚ฌ๋žŒ์€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.
๋ชจ๋‘๊ฐ€ ์ตœ์„ ์„ ๋‹คํ–ˆ๊ธฐ์—
์ ์€ ๋ฆฌ์†Œ์Šค๋กœ๋„ ๋น ๋ฅด๊ฒŒ ์‹œ์žฅ์— ๋„๋‹ฌํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋Ÿฌ๋‚˜ ํšŒ์‚ฌ๊ฐ€ ์ปค์ง€๊ณ , ์„œ๋น„์Šค๊ฐ€ ๋Š˜๊ณ , ์‚ฌ๋žŒ์ด ๋งŽ์•„์ง€๋ฉด์„œ
์ด์ „๊นŒ์ง€๋Š” ๋ฌธ์ œ๋˜์ง€ ์•Š๋˜ ๊ฒƒ๋“ค์ด ์ ์  ํ‘œ๋ฉด ์œ„๋กœ ๋“œ๋Ÿฌ๋‚˜๊ธฐ ์‹œ์ž‘ํ–ˆ์„ ๊ฒ๋‹ˆ๋‹ค.

  • โ€œ์šฐ๋ฆฌ ํด๋ผ์šฐ๋“œ ๊ณ„์ •๋“ค, ์ง€๊ธˆ ๋ˆ„๊ฐ€ ๋ฌด์Šจ ๊ถŒํ•œ์œผ๋กœ ์“ฐ๊ณ  ์žˆ์ง€?โ€
  • โ€œ์ด ์„œ๋น„์Šค, ์–ด๋А ๋„คํŠธ์›Œํฌ์—์„œ ์–ด๋–ป๊ฒŒ ๋Œ์•„๊ฐ€๋Š”์ง€ ๋‹ค๋“ค ์•„๋Š” ๊ฑฐ ๋งž๋‚˜?โ€
  • โ€œ์ด๋ฒˆ ๋‹ฌ ํด๋ผ์šฐ๋“œ ๋น„์šฉ, ์™œ ์ด๋ ‡๊ฒŒ ๋งŽ์ด ๋‚˜์™”์ง€?โ€

์ด๋Ÿฐ ์งˆ๋ฌธ๋“ค์ด ํ•˜๋‚˜๋‘˜ ์Œ“์ด๋ฉด์„œ,
๊ฒฐ๊ตญ ์ธํ”„๋ผ๋ฅผ ์ „๋‹ดํ•  ์‚ฌ๋žŒ์ด ํ•„์š”ํ•ด์กŒ๊ณ ,
๊ทธ๋ ‡๊ฒŒ ์ €๋Š” ํšŒ์‚ฌ์˜ ์ฒซ ์ธํ”„๋ผ ๋‹ด๋‹น์ž๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ ์ดํ›„ ์ œ๊ฐ€ ์–ด๋–ค ์ผ๋“ค์„ ๊ฒช์—ˆ๊ณ , ๋˜ ์–ด๋–ป๊ฒŒ ํ•˜๋‚˜์”ฉ ํ•ด๊ฒฐํ•ด ๋‚˜๊ฐ”๋Š”์ง€ ์ด์•ผ๊ธฐํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค.

๊ทธ ์ค‘์—์„œ๋„ ์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์ œ๊ฐ€ ๊ฐ€์žฅ ๋จผ์ € ๋งˆ์ฃผํ•œ ๋ฌธ์ œ์ด์ž,
์ธํ”„๋ผ ์šด์˜์˜ ์ถœ๋ฐœ์ ์ด์—ˆ๋˜ ๊ณ„์ • ๊ด€๋ฆฌ ํ˜„ํ™ฉ ์„ ์–ด๋–ป๊ฒŒ ํŒŒ์•…ํ•˜๊ณ  ์ •๋ฆฌํ•ด ๋‚˜๊ฐ”๋Š”์ง€์— ์ง‘์ค‘ํ•˜๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค.

๋‚ด๊ฐ€ ์ƒ๊ฐํ•˜๋Š” ์ธํ”„๋ผ ์—”์ง€๋‹ˆ์–ด์˜ ์—ญํ• 

์ €๋Š” ๋Œ€๊ณ ๊ฐ ์„œ๋น„์Šค๋ฅผ ํ•˜๋Š” ํšŒ์‚ฌ์—์„œ์˜ ์ธํ”„๋ผ ์—”์ง€๋‹ˆ์–ด ์—ญํ• ์„
๋‹จ์ˆœํžˆ ์„œ๋ฒ„๋‚˜ ํด๋ผ์šฐ๋“œ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์ผ๋กœ๋งŒ ๋ณด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

์™œ๋ƒํ•˜๋ฉด ์„œ๋ฒ„๋‚˜ ํด๋ผ์šฐ๋“œ ๊ด€๋ฆฌ๋งŒ์œผ๋กœ๋„ ๊ฐœ๋ฐœ์ž๋“ค์ด ์ถฉ๋ถ„ํžˆ ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ œ๊ฐ€ ํ•ฉ๋ฅ˜ํ•˜๊ธฐ ์ „๊นŒ์ง€๋„ ๊ฐœ๋ฐœํŒ€์€ ์ธํ„ฐ๋„ท์—์„œ ํ•„์š”ํ•œ ์ •๋ณด๋ฅผ ์ฐพ์•„๋ณด๊ฑฐ๋‚˜,
AI ๋“ฑ์„ ํ™œ์šฉํ•˜์—ฌ ๋ฌธ์ œ๋ฅผ ์ถฉ๋ถ„ํžˆ ์ž˜ ํ•ด๊ฒฐํ•ด์™”์—ˆ๋‹ค๊ณ  ์ƒ๊ฐํ•ฉ๋‹ˆ๋‹ค.

๋˜ํ•œ, PaaS๋‚˜ ์„œ๋ฒ„๋ฆฌ์Šค ๊ฐ™์€ ์„œ๋น„์Šค๋ฅผ ์ด์šฉํ•˜๋ฉด
๋งŽ์€ ๋ถ€๋ถ„์€ ์• ์ดˆ์— ๊ด€๋ฆฌํ•  ํ•„์š”์กฐ์ฐจ ์—†๊ฒŒ ๋งŒ๋“ค ์ˆ˜ ์žˆ๊ณ ,
์ •๋ง ์–ด๋ ต๋‹ค๋ฉด MSP ๊ฐ™์€ ์™ธ๋ถ€ ๋„์›€์„ ๋ฐ›๋Š” ์„ ํƒ์ง€๋„ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๊ทธ๋Ÿฐ๋ฐ๋„ ํšŒ์‚ฌ๋Š” ์ €๋ฅผ ์ธํ”„๋ผ ์—”์ง€๋‹ˆ์–ด๋กœ ์ฑ„์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.
๊ทธ๋ ‡๋‹ค๋ฉด ํšŒ์‚ฌ๊ฐ€ ๊ธฐ๋Œ€ํ•˜๋Š” ๋ฐ”๋Š” ๋‹จ์ˆœํ•œ ์„œ๋ฒ„๋‚˜ ํด๋ผ์šฐ๋“œ ๊ด€๋ฆฌ๊ฐ€ ์•„๋‹ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ €๋Š” ์ธํ”„๋ผ ์—”์ง€๋‹ˆ์–ด์˜ ์—ญํ• ์„ ์ด๋ ‡๊ฒŒ ์ •์˜ํ•˜๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

  • ๊ณ„์ •, ๋„คํŠธ์›Œํฌ, ๋น„์šฉ ๊ฐ™์€ ํ•ต์‹ฌ ์ž์›๋“ค์„ ํ•˜๋‚˜ํ•˜๋‚˜ ๋ณด์‚ดํ”ผ๋Š” ์‚ฌ๋žŒ
  • ๊ทธ ์•ˆ์—์„œ ์šด์˜์˜ ๊ธฐ์ค€๊ณผ ์ฒด๊ณ„๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€๋Š” ์‚ฌ๋žŒ
  • ์„œ๋น„์Šค์™€ ์กฐ์ง์ด ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ฐ˜์„ ๋‹ค์ ธ๋‚˜๊ฐ€๋Š” ์‚ฌ๋žŒ.

์ €๋Š” ์ด ์ผ์„ ๊ทธ๋Ÿฐ ๋งˆ์Œ๊ฐ€์ง์œผ๋กœ ๋งก๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค.

์ฒ˜์Œ ๋งŒ๋“  ์ธํ”„๋ผ ๊ณ„ํš

์ €๋Š” ์ธํ”„๋ผ ์šด์˜์˜ ๋ฐฉํ–ฅ์„ฑ๊ณผ ์‹คํ–‰ ๊ณ„ํš์„ ์„ธ์šฐ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ฒ˜์Œ ์„ธ์šด ๊ณ„ํš์˜ ํ•ต์‹ฌ์€ ์„ธ ๊ฐ€์ง€์˜€์Šต๋‹ˆ๋‹ค.

์ฒซ์งธ, ๊ณ„์ • ๊ด€๋ฆฌ์ž…๋‹ˆ๋‹ค.

๋ˆ„๊ฐ€ ์–ด๋–ค ๊ถŒํ•œ์„ ๊ฐ€์ง€๊ณ  ๋ฌด์—‡์„ ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ๋ช…ํ™•ํžˆ ํ•˜๊ณ ,
๋ถˆํ•„์š”ํ•œ ์ ‘๊ทผ ๊ถŒํ•œ์€ ์ค„์ด๋ฉฐ, ๊ด€๋ฆฌ์™€ ๊ฐ์‚ฌ๋ฅผ ์œ„ํ•œ ๊ธฐ์ค€์„ ๋งˆ๋ จํ•˜๋Š” ๊ฒƒ์ด ๋ชฉํ‘œ์˜€์Šต๋‹ˆ๋‹ค.

๋‘˜์งธ, ๋„คํŠธ์›Œํฌ ์„ค๊ณ„์ž…๋‹ˆ๋‹ค.

์„œ๋น„์Šค๋ณ„๋กœ ์šด์˜ ํ™˜๊ฒฝ๊ณผ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ ๋ถ„๋ฆฌํ•˜๊ณ ,
๋ณด์•ˆ ๊ทธ๋ฃน, ๋ผ์šฐํŒ…, VPN ๊ฐ™์€ ์š”์†Œ๋“ค์„ ํ†ตํ•ด
์•ˆ์ „ํ•˜๊ณ  ํšจ์œจ์ ์ธ ๋„คํŠธ์›Œํฌ ๊ตฌ์„ฑ์„ ๋งŒ๋“œ๋Š” ๋ฐฉํ–ฅ์œผ๋กœ ์žก์•˜์Šต๋‹ˆ๋‹ค.

์…‹์งธ, ๋น„์šฉ ๋ถ„์„๊ณผ ์ตœ์ ํ™”์ž…๋‹ˆ๋‹ค.

ํƒœ๊ทธ ์ฒด๊ณ„๋ฅผ ์ •๋ฆฌํ•ด ์„œ๋น„์Šค ๋ณ„ ๋น„์šฉ์„ ๊ฐ€์‹œํ™”ํ•˜๊ณ ,
๋ถˆํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฐพ์•„๋‚ด๊ฑฐ๋‚˜, ์•„ํ‚คํ…์ฒ˜ ์ฐจ์›์—์„œ ๋น„์šฉ์„ ์ค„์ผ ์ˆ˜ ์žˆ๋Š” ๋ฐฉ์•ˆ์„ ๊ณ ๋ฏผํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๊ณ„ํš๋“ค์€ ๋‹จ์ˆœํžˆ ๊ด€๋ฆฌ ๋งค๋‰ด์–ผ์„ ๋งŒ๋“œ๋Š” ๊ฒƒ์„ ๋„˜์–ด์„œ,
์šฐ๋ฆฌ ํšŒ์‚ฌ ์ธํ”„๋ผ์˜ ๊ธฐ๋ฐ˜์„ ๋งˆ๋ จํ•˜๊ณ  ์•ž์œผ๋กœ ์„ฑ์žฅํ•  ์ˆ˜ ์žˆ๋Š” ํ‹€์„ ๋งŒ๋“œ๋Š” ๋ฐ ์ดˆ์ ์„ ๋งž์ท„์Šต๋‹ˆ๋‹ค.

์‚ฌ์‹ค ์ด๋Ÿฐ ๋ถ€๋ถ„๋“ค ์ค‘ ์ƒ๋‹น์ˆ˜๋Š” ์ด๋ฏธ ์–ด๋А ์ •๋„ ์ง„ํ–‰๋˜์–ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ €๋Š” ์ œ๊ฐ€ ๋ณด๊ธฐ์— ์•„์ง ๋‹ค๋“ฌ์„ ์—ฌ์ง€๊ฐ€ ์žˆ๋Š” ๋ถ€๋ถ„๋“ค์„ ํ•˜๋‚˜ํ•˜๋‚˜ ์ฑ„์›Œ๋‚˜๊ฐ€๊ณ ,
์ œ ๊ฒฝํ—˜์„ ์‚ด๋ ค ๋” ์ฒด๊ณ„์ ์ด๊ณ  ๊ตฌ์ฒด์ ์ธ ๋ฐฉ์‹์œผ๋กœ ๋ฐœ์ „์‹œ์ผœ์•ผ๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

์•„๋ฌด๊ฒƒ๋„ ์ •ํ•ด์ง„ ๊ฒƒ์ด ์—†๋Š” ํ™˜๊ฒฝ์—์„œ ๊ทธ ์—ญํ• ์„ ์ฒ˜์Œ ๋งก๋Š”๋‹ค๋Š” ๊ฑด
๋ฌด๊ฑฐ์šด ์ฑ…์ž„๊ฐ์ด ๋จผ์ € ๋ฐ€๋ ค์˜ต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ์ด๋Ÿฐ ๊ธฐํšŒ๋Š” ๋ˆ„๊ตฌ์—๊ฒŒ๋‚˜ ์ฃผ์–ด์ง€๋Š” ๊ฑด ์•„๋‹ˆ๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๊ณ ,
๊ทธ๋งŒํผ ์–ด๋–ป๊ฒŒ ์‹œ์ž‘ํ•˜๋А๋ƒ๊ฐ€ ์ค‘์š”ํ•˜๋‹ค๋Š” ๋งˆ์Œ์œผ๋กœ ์ž„ํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ ์‹œ์ž‘์ด ๋‹จ์ˆœํžˆ ์ง€๊ธˆ๋งŒ์„ ์œ„ํ•œ ๊ด€๋ฆฌ๊ฐ€ ์•„๋‹ˆ๋ผ,
์•ž์œผ๋กœ ์˜ฌ ์‚ฌ๋žŒ๋“ค์„ ์œ„ํ•œ ๊ธฐ์ค€๊ณผ ๋ฐฉํ–ฅ์„ ์„ธ์šฐ๋Š” ์ผ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๊ณ„์ • ๊ด€๋ฆฌ, ํ˜„ํ™ฉ์„ ๊ฐ€์‹œํ™”ํ•˜๋Š” ๊ฒƒ๋ถ€ํ„ฐ

์ œ๊ฐ€ ํ•ฉ๋ฅ˜ํ•œ ์‹œ์ ์€ ์ด๋ฏธ ๋งŽ์€ ๊ฒƒ๋“ค์ด ์ง„ํ–‰๋œ ๋’ค์˜€์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ €๋Š” ์ฒ˜์Œ๋ถ€ํ„ฐ ์™„๋ฒฝํžˆ ์ •๋ฆฌํ•˜๋ ค ํ•˜๊ธฐ๋ณด๋‹ค๋Š”,
ํ˜„์žฌ ์ƒํƒœ๋ฅผ ํ•˜๋‚˜ํ•˜๋‚˜ ์ดํ•ดํ•˜๊ณ  ๋ถ€์กฑํ•œ ๋ถ€๋ถ„์„ ์‚ดํŽด๋ณด๋Š” ๋ฐ ์ง‘์ค‘ํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ ์ค‘์—์„œ ๊ฐ€์žฅ ๋จผ์ € ๋– ์˜ฌ๋ฆฐ ๊ฑด ์ธ์‚ฌ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์ด์—ˆ์Šต๋‹ˆ๋‹ค.
์–ด๋–ค ์—…๋ฌด๋“ , ๊ทธ ์ „์— ๋จผ์ € ํšŒ์‚ฌ์˜ ์ผ์›์ด ๋˜์–ด์•ผ ์‹œ์ž‘ํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์šฐ๋ฆฌ ํšŒ์‚ฌ์˜ ์ธ์‚ฌ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์€ Flex์˜€์Šต๋‹ˆ๋‹ค.

Flex โ€“ HR Service as a Software

Flex์— ๋กœ๊ทธ์ธํ•  ๋•Œ๋ถ€ํ„ฐ Google ๊ณ„์ •์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ธ์ฆ์ด ์ด๋ฃจ์–ด์ง„๋‹ค๋Š” ์ ์ด ๋ˆˆ์— ๋„์—ˆ์Šต๋‹ˆ๋‹ค.
์ด ๋ง์€ ๊ฒฐ๊ตญ, ๋ชจ๋“  ๊ณ„์ • ๊ด€๋ฆฌ์˜ ์ถœ๋ฐœ์ ์ด Google Workspace ๋ผ๋Š” ๋œป์ด์—ˆ์Šต๋‹ˆ๋‹ค.

๋‹ค๋งŒ ์ด ์˜์—ญ์€ ์ธ์‚ฌํŒ€์—์„œ ์ง์ ‘ ๊ด€๋ฆฌํ•˜๊ณ  ์žˆ์—ˆ๊ธฐ์—,
์ œ๊ฐ€ ๋ฐ”๋กœ ๊ฐœ์ž…ํ•  ์ˆ˜๋Š” ์—†์—ˆ๊ณ ,
์ผ๋‹จ์€ ํ˜„์žฌ ์–ด๋–ค ๊ณ„์ •๋“ค์ด ์กด์žฌํ•˜๋Š”์ง€ ํŒŒ์•…ํ•˜๋Š” ๊ฒƒ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

Terraform์œผ๋กœ ์‹œ์ž‘ํ•œ ์ธํ”„๋ผ ์ž๋™ํ™”

๊ถŒํ•œ์„ ๋ฐ›์•„ Google Admin ์ฝ˜์†”์— ์ ‘์†ํ•ด ๊ณ„์ •๊ณผ ๊ถŒํ•œ ํ˜„ํ™ฉ ํŒŒ์•…์„ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํšŒ์‚ฌ์˜ ๊ตฌ๊ธ€ ๊ณ„์ •๊ณผ ๊ทธ๋ฃน ๊ณ„์ •๋“ค์— ๋Œ€ํ•œ ์ •๋ณด๋“ค์„ Google Sheets์— ์ˆ˜์ž‘์—…์œผ๋กœ ์ •๋ฆฌํ•˜๊ณ  ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

ํ•˜์ง€๋งŒ ํ˜ผ์ž์„œ ์ผํ•˜๋Š” ์ƒํ™ฉ์—์„œ ํ•˜๋‚˜ํ•˜๋‚˜ ์ˆ˜๋™์œผ๋กœ ์ •๋ฆฌํ•˜๋Š” ๊ฑด ๋„ˆ๋ฌด ๋น„ํšจ์œจ์ ์ด๋ผ๊ณ  ๋А๊ผˆ์Šต๋‹ˆ๋‹ค.
๋ฌด์—‡๋ณด๋‹ค ์ €๋„ ์‚ฌ๋žŒ์ด๊ธฐ์—, ์‹ค์ˆ˜ํ•˜๊ฑฐ๋‚˜ ๋†“์น˜๋Š” ๋ถ€๋ถ„์ด ์ƒ๊ธธ ์ˆ˜ ์žˆ๋‹ค๋Š” ์ ๋„ ๊ณ„์† ์šฐ๋ ค๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ธํ”„๋ผ๋ฅผ ์ฝ”๋“œ๋กœ ๊ด€๋ฆฌํ•ด์•ผ๊ฒ ๋‹ค๊ณ  ์ƒ๊ฐํ–ˆ์Šต๋‹ˆ๋‹ค.

Terraform

Terraform์€ HashiCorp์—์„œ ๋งŒ๋“  ์ธํ”„๋ผ๋ฅผ ์ฝ”๋“œ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋„๊ตฌ (IaC: Infrastructure as Code)์ž…๋‹ˆ๋‹ค.

ํด๋ผ์šฐ๋“œ, ์˜จํ”„๋ ˆ๋ฏธ์Šค, SaaS ๋“ฑ ๋‹ค์–‘ํ•œ ํ™˜๊ฒฝ์˜ ์ธํ”„๋ผ ๋ฆฌ์†Œ์Šค๋ฅผ ์ฝ”๋“œ๋กœ ์„ ์–ธํ•˜๊ณ ,
๊ทธ ์„ ์–ธ ์ƒํƒœ๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์ž๋™์œผ๋กœ ์ƒ์„ฑ, ๋ณ€๊ฒฝ, ์‚ญ์ œํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค๋‹ˆ๋‹ค.

ํ˜„ํ™ฉ ํŒŒ์•…์„ ์ž๋™ํ™”ํ•  ์ˆ˜ ์žˆ๋Š” ๋„๊ตฌ๋Š” ์—ฌ๋Ÿฌ ๊ฐ€์ง€๊ฐ€ ์žˆ์ง€๋งŒ,
์ €๋Š” Terraform์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

์ด์œ ๋Š” ๋ช…ํ™•ํ–ˆ์Šต๋‹ˆ๋‹ค.

  • ํ˜„์žฌ ์ธํ”„๋ผ ๊ตฌ์„ฑ์ด ์ฝ”๋“œ๋กœ ๋ช…์‹œ๋˜์–ด ๋ˆ„๊ตฌ๋‚˜ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์Œ
  • Git ๊ฐ™์€ ๋ฒ„์ „ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์œผ๋กœ ๋ณ€๊ฒฝ ์ด๋ ฅ์„ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์Œ
  • ํ˜„ํ™ฉ ํŒŒ์•…๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๋ณธ๋ž˜ ๋ชฉ์ ์ธ ๋ฆฌ์†Œ์Šค ๊ด€๋ฆฌ๊นŒ์ง€๋„ ํ™•์žฅ ๊ฐ€๋Šฅํ•จ
  • ๋‹ค์–‘ํ•œ ํ”Œ๋žซํผ์„ ํ•˜๋‚˜์˜ ๋„๊ตฌ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Œ

๊ทธ๋ ‡๊ฒŒ ์ €๋Š” Terraform์œผ๋กœ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•˜๊ธฐ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

Google Workspace ๊ณ„์ • ํ˜„ํ™ฉ ํŒŒ์•… ์‹œ์ž‘

๋ณธ๊ฒฉ์ ์œผ๋กœ Terraform ์ฝ”๋“œ๋ฅผ ํ†ตํ•ด Google Workspace ๊ณ„์ • ํ˜„ํ™ฉ ํŒŒ์•…์„ ์‹œ์ž‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

Terraform - Google Workspace Provider

Terraform์—์„œ Google Workspace Provider๋ฅผ ์‚ฌ์šฉํ•˜๋ ค๋ฉด,
Google Cloud์™€ Google Workspace์˜ ์‚ฌ์ „ ์„ค์ •์ด ํ•„์š”ํ–ˆ์Šต๋‹ˆ๋‹ค.

์„œ๋น„์Šค ๊ณ„์ •์„ ๋งŒ๋“ค๊ณ , ํ•„์š”ํ•œ OAuth ๊ถŒํ•œ์„ ๋ถ€์—ฌํ•˜๊ณ ,
๊ด€๋ฆฌ์ž ๊ณ„์ •์œผ๋กœ์˜ ์œ„์ž„ ์„ค์ •๊นŒ์ง€ ๋งˆ์ณ์•ผ
Terraform ์ฝ”๋“œ๋กœ Google Workspace์˜ ์ฝ๊ธฐ ์ž‘์—…์ด ๊ฐ€๋Šฅํ–ˆ์Šต๋‹ˆ๋‹ค.
(์ฐธ์กฐ: Manage your Google Workspace organization)

์•„๋ž˜ ์ฝ”๋“œ๋Š” ํ•ด๋‹น ์ž‘์—…์„ Terraform์œผ๋กœ ๊ตฌํ˜„ํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

# Google Workspace Provider ์„ค์ •
provider "googleworkspace" {
  customer_id             = "***"  # Google Workspace customer ID (์กฐ์ง ์‹๋ณ„์ž)
  impersonated_user_email = "jiyoung.park@wonderwall.kr"  # API ํ˜ธ์ถœ ์‹œ ์‚ฌ์šฉํ•  ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ด๋ฉ”์ผ

  credentials = pathexpand(
    "/Users/jiyoung.park/.google/credentials/wonderwall-infra-00.json"
  )  # ์„œ๋น„์Šค ๊ณ„์ • JSON ํ‚ค ํŒŒ์ผ ๊ฒฝ๋กœ

  oauth_scopes = [
    "https://www.googleapis.com/auth/admin.directory.domain.readonly",         # ๋„๋ฉ”์ธ ์ฝ๊ธฐ ์ „์šฉ
    "https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly", # ์—ญํ•  ๊ด€๋ฆฌ ์ฝ๊ธฐ ์ „์šฉ
    "https://www.googleapis.com/auth/admin.directory.orgunit",                 # ์กฐ์ง ๋‹จ์œ„ ์ฝ๊ธฐ/์“ฐ๊ธฐ
    "https://www.googleapis.com/auth/admin.directory.group",                   # ๊ทธ๋ฃน ์ฝ๊ธฐ/์“ฐ๊ธฐ
    "https://www.googleapis.com/auth/admin.directory.user"                     # ์‚ฌ์šฉ์ž ์ฝ๊ธฐ/์“ฐ๊ธฐ
  ]  # Google API ํ˜ธ์ถœ์— ํ•„์š”ํ•œ OAuth ์Šค์ฝ”ํ”„ ๋ชฉ๋ก
}

# Google Workspace ์‚ฌ์šฉ์ž ์ „์ฒด ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
data "googleworkspace_users" "this" {
  provider = googleworkspace
}

# ๊ฐ€์ ธ์˜จ ์‚ฌ์šฉ์ž ๋ชฉ๋ก์—์„œ primary_email(์ด๋ฉ”์ผ)๋ณ„๋กœ ๊ฐœ๋ณ„ user ๋ฐ์ดํ„ฐ ์†Œ์Šค ์ƒ์„ฑ
data "googleworkspace_user" "email" {
  provider = googleworkspace

  for_each = toset([
    for user in data.googleworkspace_users.this.users
    : user.primary_email  # ๊ฐ ์‚ฌ์šฉ์ž์˜ ์ด๋ฉ”์ผ ์ฃผ์†Œ ์ถ”์ถœ
  ])

  primary_email = each.value  # ๊ฐœ๋ณ„ user ๋ฐ์ดํ„ฐ ์†Œ์Šค์˜ primary_email ์„ค์ •
}
...

์ด๋ ‡๊ฒŒ ํ•ด์„œ Google Workspace ๊ณ„์ • ๋ชฉ๋ก์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ ,
Terraform์„ ํ†ตํ•ด ๊ฐ€์‹œ์„ฑ์„ ํ™•๋ณดํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๊ณต์‹ Provider๋กœ๋Š” ๋ถ€์กฑํ•œ ๋ถ€๋ถ„๋“ค

Terraform์„ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋ฉด, ๊ณต์‹ ํ”„๋กœ๋ฐ”์ด๋”์—์„œ ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด, Google Workspace ์‚ฌ์šฉ์ž๋ณ„ MFA(๋‹ค๋‹จ๊ณ„ ์ธ์ฆ) ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ด์•ผ ํ–ˆ๋Š”๋ฐ,
๊ณต์‹ Google Workspace ํ”„๋กœ๋ฐ”์ด๋”์—๋Š” ์ด๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ฆฌ์†Œ์Šค๋‚˜ ๋ฐ์ดํ„ฐ ์†Œ์Šค๊ฐ€ ์—†์—ˆ์Šต๋‹ˆ๋‹ค.

์ €๋Š” ์ด ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด external์„ ํ™œ์šฉํ–ˆ์Šต๋‹ˆ๋‹ค.

Terraform - external

external ๋ฐ์ดํ„ฐ ์†Œ์Šค๋Š” Terraform ์™ธ๋ถ€์—์„œ ์‹คํ–‰๋œ ์Šคํฌ๋ฆฝํŠธ์˜ ์ถœ๋ ฅ์„ Terraform์— ๊ฐ’์œผ๋กœ ๋„˜๊ฒจ์ค„ ์ˆ˜ ์žˆ๋„๋ก ๋„์™€์ค๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋Š” ํ•ด๋‹น ์ž‘์—…์„ ๊ตฌํ˜„ํ•œ Bash ์Šคํฌ๋ฆฝํŠธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

#!/bin/bash

set -euo pipefail  # ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์ฆ‰์‹œ ์ข…๋ฃŒ, unset ๋ณ€์ˆ˜ ์‚ฌ์šฉ ๊ธˆ์ง€, ํŒŒ์ดํ”„๋ผ์ธ ์—๋Ÿฌ ์ฒดํฌ

# ์ž…๋ ฅ ์ธ์ž
IMPERSONATED_USER_EMAIL="$1"  # API ํ˜ธ์ถœ ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ด๋ฉ”์ผ
CREDENTIAL_FILE="$2"          # ์„œ๋น„์Šค ๊ณ„์ • ํ‚ค JSON ํŒŒ์ผ ๊ฒฝ๋กœ
OAUTH_SCOPES="$3"             # OAuth scope ๋ฆฌ์ŠคํŠธ (๊ณต๋ฐฑ ๊ตฌ๋ถ„)
USER_EMAIL="$4"               # MFA ์ƒํƒœ๋ฅผ ์กฐํšŒํ•  ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ

# base64url ์ธ์ฝ”๋”ฉ ํ•จ์ˆ˜ (JWT์šฉ)
base64url() {
  openssl base64 -e -A | tr '+/' '-_' | tr -d '='
}

# JWT ํ† ํฐ ๋ฐœ๊ธ‰์šฉ ํƒ€์ž„์Šคํƒฌํ”„
ISSUED_AT=$(date +%s)
EXPIRATION=$((ISSUED_AT + 30))  # 30์ดˆ ์œ ํšจ

# ์„œ๋น„์Šค ๊ณ„์ • ์ •๋ณด ์ฝ๊ธฐ
CLIENT_EMAIL=$(jq -r .client_email "$CREDENTIAL_FILE")
PRIVATE_KEY=$(jq -r .private_key "$CREDENTIAL_FILE")

# JWT ํ—ค๋” ์ƒ์„ฑ
HEADER_BASE64=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64url)

# JWT ํด๋ ˆ์ž„ ์ƒ์„ฑ
CLAIMS=$(jq -nc --arg iss "$CLIENT_EMAIL" --arg sub "$IMPERSONATED_USER_EMAIL" \
  --arg aud "https://oauth2.googleapis.com/token" --arg scope "$OAUTH_SCOPES" \
  --argjson iat "$ISSUED_AT" --argjson exp "$EXPIRATION" \
  '{iss: $iss, scope: $scope, aud: $aud, exp: $exp, iat: $iat, sub: $sub}')
CLAIMS_BASE64=$(echo -n "$CLAIMS" | base64url)

# JWT ์„œ๋ช… ์ž…๋ ฅ๊ฐ’
SIGN_INPUT="${HEADER_BASE64}.${CLAIMS_BASE64}"

# RSA ์„œ๋ช… ์ƒ์„ฑ
TMP_KEY=$(mktemp)
trap 'rm -f "$TMP_KEY"' EXIT
echo "$PRIVATE_KEY" | sed 's/\\n/\n/g' > "$TMP_KEY"
SIGNED=$(echo -n "$SIGN_INPUT" | openssl dgst -sha256 -sign "$TMP_KEY" | base64url)

# ์ตœ์ข… JWT ํ† ํฐ
JWT="${SIGN_INPUT}.${SIGNED}"

# ์•ก์„ธ์Šค ํ† ํฐ ์š”์ฒญ
RESPONSE=$(curl -s -X POST "https://oauth2.googleapis.com/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" -d "assertion=$JWT")
TOKEN=$(echo "$RESPONSE" | jq -r '.access_token')

# ํ† ํฐ ์œ ํšจ์„ฑ ์ฒดํฌ
if [[ -z "$TOKEN" || "$TOKEN" == "null" ]]; then
  exit 1
fi

# Google Admin API์—์„œ MFA ์ƒํƒœ ์กฐํšŒ
USER_MFA_STATUS=$(curl -s -H "Authorization: Bearer $TOKEN" \
  "https://admin.googleapis.com/admin/directory/v1/users/$USER_EMAIL" \
  | jq -r 'if .isEnrolledIn2Sv then "enrolled" elif .isEnforcedIn2Sv then "enforced"
    else "none" end')

# Terraform external๋กœ ๋ฐ˜ํ™˜ํ•  JSON ์ถœ๋ ฅ
echo "{\"string\": \"$USER_MFA_STATUS\"}"

์ด์–ด์„œ, ํ•ด๋‹น Bash ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” Terraform ์ฝ”๋“œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

# ์„œ๋น„์Šค ๊ณ„์ • ์ธ์ฆ ์ •๋ณด ์ž…๋ ฅ ๋ณ€์ˆ˜ ์ •์˜
variable "credential" {
  description = "A map containing credential values."

  type = object({
    impersonated_user_email = string  # ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ด๋ฉ”์ผ
    file                    = string  # ์„œ๋น„์Šค ๊ณ„์ • ํ‚ค ํŒŒ์ผ ๊ฒฝ๋กœ
    oauth_scopes            = list(string)  # OAuth scope ๋ฆฌ์ŠคํŠธ
  })
}

# ๊ฐ ์‚ฌ์šฉ์ž๋ณ„ MFA ์ƒํƒœ๋ฅผ ์™ธ๋ถ€ ์Šคํฌ๋ฆฝํŠธ๋กœ ์กฐํšŒ
data "external" "googleworkspace_user_mfa_status" {
  for_each = {
    for user_email, user_attributes in data.googleworkspace_user.email
    : user_email => user_email  # ์ด๋ฉ”์ผ๋ณ„ loop
  }

  program = [
    "bash", format("%s/bash/user_mfa_status.sh", path.module),  # ์‹คํ–‰ํ•  bash ์Šคํฌ๋ฆฝํŠธ ๊ฒฝ๋กœ
    var.credential.impersonated_user_email,  # ๊ด€๋ฆฌ์ž ๊ณ„์ • ์ด๋ฉ”์ผ
    pathexpand(var.credential.file),         # ์„œ๋น„์Šค ๊ณ„์ • ํ‚ค ๊ฒฝ๋กœ
    join(" ", var.credential.oauth_scopes),  # OAuth scope ๋ฌธ์ž์—ด๋กœ ๋ณ‘ํ•ฉ
    each.value                               # ํ˜„์žฌ ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ
  ]
}
...

external์—๋Š” ํฐ ์ œํ•œ ์‚ฌํ•ญ์ด ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.
๋ชจ๋‘ JSON ํ˜•์‹์œผ๋กœ๋งŒ ์ฒ˜๋ฆฌํ•ด์•ผ ๋œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

JSON์— ์ต์ˆ™ํ•˜์ง€ ์•Š์•˜๋˜ ์ €๋Š” ์‹œํ–‰์ฐฉ์˜ค๋ฅผ ๋งŽ์ด ๊ฒช์—ˆ์ง€๋งŒ,
์ตœ์ข…์ ์œผ๋กœ data.external.googleworkspace_user_mfa_status[์ด๋ฉ”์ผ์ฃผ์†Œ].result.string ์†์„ฑ์—์„œ
MFA ์ƒํƒœ ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

SSO๋กœ ์—ฐ๊ฒฐ๋œ ์„œ๋น„์Šค, ์—ฐ๊ฒฐ๋˜์ง€ ์•Š์€ ์„œ๋น„์Šค

Google Workspace ๊ณ„์ • ํ˜„ํ™ฉ์„ ํŒŒ์•…ํ•œ ๋’ค,
๋‹ค๋ฅธ ์„œ๋น„์Šค๋“ค์€ ์–ด๋–ค ์ƒํƒœ์ธ์ง€๋„ ์‚ดํŽด๋ณด์•˜์Šต๋‹ˆ๋‹ค.

ํฌ๊ฒŒ ๋‘ ๊ฐ€์ง€๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋ฐ”๋กœ Google ๊ณ„์ •(SSO)๊ณผ ์—ฐ๋™๋˜๋Š” ์„œ๋น„์Šค์ธ๊ฐ€, ์•„๋‹Œ๊ฐ€์˜€์Šต๋‹ˆ๋‹ค.

  • SSO ์‚ฌ์šฉ: Flex, Slack, Atlassian ๋“ฑ
  • SSO ๋ฏธ์‚ฌ์šฉ: AWS, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ์„œ๋น„์Šค ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ๋“ฑ

๋ถ„๋ฅ˜ํ•œ ์ด ํ›„, ์ฒ˜์Œ๋ถ€ํ„ฐ ๋ชจ๋“  ์„œ๋น„์Šค๋ฅผ ์ „๋ถ€ ์ •๋ฆฌํ•˜๋ ค๊ณ  ํ•˜์ง€๋Š” ์•Š์•˜์Šต๋‹ˆ๋‹ค.

์ธํ”„๋ผ ์—”์ง€๋‹ˆ์–ด๋กœ์„œ ๊ฐ€์žฅ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์€ ์˜์—ญ, ์ฆ‰ AWS๋ถ€ํ„ฐ ํ•˜๋‚˜์”ฉ ์ •๋ฆฌํ•ด๋ณด๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

AWS๋Š” SSO๋กœ ์—ฐ๋™๋˜์–ด ์žˆ์ง€ ์•Š์•˜๊ณ ,
Google ๊ณ„์ •์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ˆ˜๋™ ๊ด€๋ฆฌ ํ˜•ํƒœ์˜€์Šต๋‹ˆ๋‹ค.

์ด ์ ์€ ์ดํ›„ ๊ณ„์ • ํ˜„ํ™ฉ ์ž‘์—…์˜ ์ค‘์š”ํ•œ ํ™•์ธ ํฌ์ธํŠธ๊ฐ€ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

AWS ๊ณ„์ • ํ˜„ํ™ฉ ํŒŒ์•…, ์—ฐ๊ฒฐ ๊ณ ๋ฆฌ๋ถ€ํ„ฐ ์ฐพ๋‹ค

AWS ์ธํ”„๋ผ ๊ด€๋ฆฌ ์ž์ฒด๋Š” ์ด๋ฏธ ์—…๊ณ„์—์„œ ๋ณดํŽธํ™”๋˜์–ด ์žˆ์—ˆ๊ธฐ์—,
Terraform์œผ๋กœ ํ˜„ํ™ฉ์„ ํ™•์ธํ•˜๋Š” ์ผ์€ ๊ทธ๋ฆฌ ์–ด๋ ต์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.

AWS Provider

๋‹ค๋งŒ, ๋‹น์‹œ ์ €๋Š” ์ž…์‚ฌํ•œ ์ง€ 3๊ฐœ์›”๋„ ์ฑ„ ๋˜์ง€ ์•Š์€ ์ˆ˜์Šต ๊ธฐ๊ฐ„์ด์—ˆ๊ธฐ ๋•Œ๋ฌธ์—,
์™œ ๋ˆ„๊ตฐ๊ฐ€์—๊ฒŒ ํŠน์ • ๊ถŒํ•œ๋“ค์ด ๋ถ€์—ฌ๋๋Š”์ง€๊นŒ์ง€๋Š” ์ดํ•ดํ•˜๊ธฐ ์–ด๋ ค์› ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ €๋Š” ์šฐ์„  โ€œ์™œ ์ด ๊ณ„์ •์„ ๋งŒ๋“ค์—ˆ๋Š”์ง€โ€ ๊ฐ™์€ ๋ฐฐ๊ฒฝ์€ ๋’ค๋กœ ๋ฏธ๋ฃจ๊ณ ,
์ˆœ์ˆ˜ํžˆ ํ˜„ํ™ฉ ํŒŒ์•…์—๋งŒ ์ง‘์ค‘ํ•˜๊ธฐ๋กœ ํ–ˆ์Šต๋‹ˆ๋‹ค.

ํ˜„์žฌ ๋งŒ๋“ค์–ด์ง„ AWS ๊ณ„์ •๋“ค์ด ์–ด๋– ํ•œ ๊ฒƒ๋“ค์ด ์žˆ๊ณ ,
Google Workspace ๊ณ„์ •๊ณผ๋Š” ์–ด๋–ค ์—ฐ๊ฒฐ ๊ณ ๋ฆฌ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ด์—ˆ์Šต๋‹ˆ๋‹ค.

ํ˜„ํ™ฉ์„ ํ™•์ธํ•ด ๋ณด๋‹ˆ,
Google Workspace์™€ AWS ๋ชจ๋‘ ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž ๊ณ„์ •๊ณผ ๋น„์‚ฌ์šฉ์ž ๊ณ„์ •์ด ์„ž์—ฌ ์žˆ๋Š” ์ƒํƒœ์˜€์Šต๋‹ˆ๋‹ค.

AWS ์ชฝ์—๋Š” ๋‹น์—ฐํžˆ ์„œ๋น„์Šค๋‚˜ ์‹œ์Šคํ…œ ์šด์˜์„ ์œ„ํ•œ ๊ณ„์ •๋“ค์ด ์žˆ์„ ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐํ–ˆ์ง€๋งŒ,
Google Workspace์—๋„ ๋ณ„๋„๋กœ ๋งŒ๋“ค์–ด์ง„ ๊ด€๋ฆฌ์šฉ ๊ณ„์ •์ด ์กด์žฌํ•œ๋‹ค๋Š” ์ ์€ ์ฒ˜์Œ์—” ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.

๊ทธ๋ž˜์„œ ์ €๋Š” ๋จผ์ € Google Workspace ๊ณ„์ • ID๋ฅผ ๊ธฐ์ค€์œผ๋กœ ์‚ฌ์šฉ์ž ๊ณ„์ •์„ ํ•„ํ„ฐ๋งํ–ˆ๊ณ ,
๊ทธ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ AWS IAM ๊ณ„์ •๊ณผ ๋งค์นญํ•˜์—ฌ ์‹ค์ œ ์‚ฌ์šฉ์ž ๊ณ„์ •๊ณผ ๋น„์‚ฌ์šฉ์ž ๊ณ„์ •์„ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋Š” ํ•ด๋‹น ์ž‘์—…์„ Terraform์œผ๋กœ ๊ตฌํ˜„ํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

# ์‹œ์Šคํ…œ ๊ณ„์ • ๋ฐ ์„œ๋น„์Šค ๊ณ„์ • ์‹๋ณ„์šฉ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก
locals {
  non_user_keywords = [
    "admin", "no-reply", "backup", "dev",
    "message", "ops", "vpn", "support", 
    "info", "artlab", "stage", "test", "bot"
  ]
}

# ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ์„ ํ‚ค์›Œ๋“œ ํฌํ•จ ์—ฌ๋ถ€๋กœ ๋ถ„๋ฅ˜
locals {
  # Google Workspace ์‚ฌ์šฉ์ž๋ณ„๋กœ ์‹ค์ œ ์‚ฌ์šฉ์ž์ธ์ง€ ์‹œ์Šคํ…œ ๊ณ„์ •์ธ์ง€ ๋ถ„๋ฅ˜
  email_classification = {
    for email, attributes in data.googleworkspace_user.email
    : email => {
      primary_email = attributes.primary_email
      # ํ‚ค์›Œ๋“œ๊ฐ€ ํฌํ•จ๋˜์ง€ ์•Š์œผ๋ฉด ์‹ค์ œ ์‚ฌ์šฉ์ž๋กœ ๋ถ„๋ฅ˜
      is_user = !anytrue([
        for keyword in local.non_user_keywords
        : can(regex("(?i)${keyword}", email))
      ])
    }
  }
  
  # ์‹ค์ œ ์‚ฌ์šฉ์ž ์ด๋ฉ”์ผ๋งŒ ์ถ”์ถœ
  user_emails = {
    for email, data in local.email_classification
    : email => data.primary_email
    if data.is_user
  }
  
  # ์‹œ์Šคํ…œ/์„œ๋น„์Šค ๊ณ„์ • ์ด๋ฉ”์ผ๋งŒ ์ถ”์ถœ
  non_user_emails = {
    for email, data in local.email_classification
    : email => data.primary_email
    if !data.is_user
  }
}

# AWS IAM ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์†Œ์Šค (์‹ค์ œ ์‚ฌ์šฉ์ž๋งŒ)
data "aws_iam_user" "user" {
  provider = aws
  for_each = local.user_emails
  user_name = each.value
}

# AWS IAM ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ์†Œ์Šค (์‹œ์Šคํ…œ/์„œ๋น„์Šค ๊ณ„์ •)
data "aws_iam_user" "non_user" {
  provider = aws
  for_each = local.non_user_emails
  user_name = each.value
}
...

์ด๋Ÿฐ ๋ฐฉ์‹์œผ๋กœ Google Workspace ๊ณ„์ •๊ณผ AWS ๊ณ„์ • ๊ฐ„ ์—ฐ๊ฒฐ ๊ณ ๋ฆฌ๋ฅผ ์‹๋ณ„ํ–ˆ๊ณ ,
์‹ค์ œ ์‚ฌ์šฉ์ž ๊ณ„์ •๊ณผ ์‹œ์Šคํ…œ ๋˜๋Š” ์„œ๋น„์Šค ์šฉ ๊ณ„์ •์„ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.


๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณ„์ •, ๋™์ผํ•œ ๊ธฐ์ค€์œผ๋กœ ๋ถ„๋ฅ˜ํ•˜๋‹ค

AWS ๊ณ„์ • ํ˜„ํ™ฉ์„ ์ •๋ฆฌํ•œ ๋’ค์—๋Š” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณ„์ •์œผ๋กœ ์‹œ์„ ์„ ์˜ฎ๊ฒผ์Šต๋‹ˆ๋‹ค.

์šฐ๋ฆฌ ํšŒ์‚ฌ์—์„œ ์‚ฌ์šฉํ•˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋Š” RDS, ๊ทธ์ค‘์—์„œ๋„ Aurora PostgreSQL์ž…๋‹ˆ๋‹ค.

PostgreSQL ์šฉ Terraform provider๋Š” ์กด์žฌํ•˜์ง€๋งŒ,
์ œ๊ฐ€ ํ•„์š”๋กœ ํ–ˆ๋˜ ๊ณ„์ • ํ˜„ํ™ฉ์„ ์กฐํšŒํ•˜๋Š” ๋ฐ์ดํ„ฐ ์†Œ์Šค๋Š” ์ง€์›ํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.
๊ทธ๋ž˜์„œ ์ €๋Š” ๋‹ค์‹œ ํ•œ๋ฒˆ Bash ์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ตํ•ด ์ง์ ‘ ์ฟผ๋ฆฌ๋ฅผ ์‹คํ–‰ํ•˜๋Š” external ๋ฐ์ดํ„ฐ ์†Œ์Šค ๋ฐฉ์‹์„ ์„ ํƒํ–ˆ์Šต๋‹ˆ๋‹ค.

๋ฌผ๋ก  ์ด ๋ฐฉ์‹์ด ์ฝ”๋“œ๊ฐ€ ๋น„๊ต์  ๊ธธ์–ด์ง€๊ณ  ๋ณต์žกํ•ด์ง„๋‹ค๋Š” ๋‹จ์ ์€ ์žˆ์ง€๋งŒ,
Terraform์ด๋ผ๋Š” ํ‘œ์ค€ํ™”๋œ ๊ด€๋ฆฌ ์ฒด๊ณ„ ์•ˆ์—์„œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ณ„์ •๊นŒ์ง€ ํฌํ•จ์‹œํ‚ค๊ธฐ ์œ„ํ•œ ๊ฒฐ์ •์ด์—ˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋Š” PostgreSQL์—์„œ ๋กœ๊ทธ์ธ ๊ฐ€๋Šฅํ•œ ์‚ฌ์šฉ์ž ๊ณ„์ • ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” Bash ์Šคํฌ๋ฆฝํŠธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

#!/bin/bash
# ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ์‹œ ์ฆ‰์‹œ ์ข…๋ฃŒ, ์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜ ์‚ฌ์šฉ์‹œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
set -euo pipefail

# ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ›์€ PostgreSQL ์ ‘์† ์ •๋ณด JSON
POSTGRES_CREDENTIAL="$1"

# JSON์—์„œ PostgreSQL ์ ‘์† ์ •๋ณด ์ถ”์ถœ
POSTGRES_HOST=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.host')
POSTGRES_PORT=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.port')
POSTGRES_USERNAME=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.username')
POSTGRES_PASSWORD=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.password')

# ๋กœ๊ทธ์ธ ๊ฐ€๋Šฅํ•œ ์‚ฌ์šฉ์ž ์—ญํ•  ์กฐํšŒ ์ฟผ๋ฆฌ (์‹œ์Šคํ…œ ์—ญํ•  ์ œ์™ธ)
POSTGRES_QUERY=$(cat << EOF
SELECT rolname FROM pg_roles 
WHERE rolname NOT LIKE 'pg_%'
  AND rolname NOT LIKE 'rds%'
  AND rolcanlogin = true
ORDER BY rolname;
EOF
)

# ํฌํŠธ๊ฐ€ null์ธ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ 5432๋กœ ์„ค์ •
if [ "$POSTGRES_PORT" == "null" ] ; then
  POSTGRES_PORT="5432"
fi

# PostgreSQL ์ฟผ๋ฆฌ ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ๋ฅผ JSON ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜
POSTGRES_QUERY_ESCAPED_OUTPUT=$(PGPASSWORD=$POSTGRES_PASSWORD psql \
  -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" \
  -d postgres -U "$POSTGRES_USERNAME" \
  -tAqX -P footer=off -c "$POSTGRES_QUERY" \
  | jq -cRs 'split("\n") | map(select(length > 0)) | @json')

# Terraform external data source ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ์ถœ๋ ฅ
echo "{\"string\": $POSTGRES_QUERY_ESCAPED_OUTPUT}"

์•„๋ž˜ ์ฝ”๋“œ๋Š” ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ ํ• ๋‹น๋œ PostgreSQL ์—ญํ•  ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” Bash ์Šคํฌ๋ฆฝํŠธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

#!/bin/bash
# ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ์‹œ ์ฆ‰์‹œ ์ข…๋ฃŒ, ์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜ ์‚ฌ์šฉ์‹œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
set -euo pipefail

# ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ›์€ PostgreSQL ์ ‘์† ์ •๋ณด JSON
POSTGRES_CREDENTIAL="$1"
# ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ›์€ ์กฐํšŒํ•  ์‚ฌ์šฉ์ž๋ช…
POSTGRES_QUERY_USER_NAME="$2"

# JSON์—์„œ PostgreSQL ์ ‘์† ์ •๋ณด ์ถ”์ถœ
POSTGRES_HOST=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.host')
POSTGRES_PORT=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.port')
POSTGRES_USERNAME=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.username')
POSTGRES_PASSWORD=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.password')

# ํŠน์ • ์‚ฌ์šฉ์ž์—๊ฒŒ ํ• ๋‹น๋œ ์—ญํ•  ๋ชฉ๋ก ์กฐํšŒ ์ฟผ๋ฆฌ
POSTGRES_QUERY=$(cat << EOF
SELECT r.rolname as role_name
FROM pg_roles r
JOIN pg_auth_members m ON r.oid = m.roleid
JOIN pg_roles u ON m.member = u.oid
WHERE u.rolname = :'user'
ORDER BY r.rolname;
EOF
)

# ํฌํŠธ๊ฐ€ null์ธ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ 5432๋กœ ์„ค์ •
if [ "$POSTGRES_PORT" == "null" ] ; then
  POSTGRES_PORT="5432"
fi

# PostgreSQL ์ฟผ๋ฆฌ ์‹คํ–‰, ๊ฒฐ๊ณผ๋ฅผ ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ ํ›„ JSON ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜
POSTGRES_QUERY_ESCAPED_OUTPUT=$(PGPASSWORD=$POSTGRES_PASSWORD psql \
  -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" \
  -d postgres -U "$POSTGRES_USERNAME" -v user="$POSTGRES_QUERY_USER_NAME" \
  -tAqX -P footer=off -c "$POSTGRES_QUERY" \
  | tr [:upper:] [:lower:] | jq -cRs 'split("\n") | map(select(length > 0)) | @json')

# Terraform external data source ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ์ถœ๋ ฅ
echo "{\"string\": $POSTGRES_QUERY_ESCAPED_OUTPUT}"

์•„๋ž˜ ์ฝ”๋“œ๋Š” ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋Š” Bash ์Šคํฌ๋ฆฝํŠธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

#!/bin/bash
# ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ์‹œ ์ฆ‰์‹œ ์ข…๋ฃŒ, ์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜ ์‚ฌ์šฉ์‹œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
set -euo pipefail

# ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ›์€ PostgreSQL ์ ‘์† ์ •๋ณด JSON
POSTGRES_CREDENTIAL="$1"

# JSON์—์„œ PostgreSQL ์ ‘์† ์ •๋ณด ์ถ”์ถœ
POSTGRES_HOST=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.host')
POSTGRES_PORT=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.port')
POSTGRES_USERNAME=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.username')
POSTGRES_PASSWORD=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.password')

# ํ˜„์žฌ ์‚ฌ์šฉ์ž๊ฐ€ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชฉ๋ก ์กฐํšŒ ์ฟผ๋ฆฌ (ํ…œํ”Œ๋ฆฟ DB ์ œ์™ธ)
POSTGRES_QUERY=$(cat << EOF
SELECT datname FROM pg_database 
WHERE datistemplate = false 
AND has_database_privilege(current_user, datname, 'CONNECT') 
ORDER BY datname;
EOF
)

# ํฌํŠธ๊ฐ€ null์ธ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ 5432๋กœ ์„ค์ •
if [ "$POSTGRES_PORT" == "null" ] ; then
  POSTGRES_PORT="5432"
fi

# PostgreSQL ์ฟผ๋ฆฌ ์‹คํ–‰ ๋ฐ ๊ฒฐ๊ณผ๋ฅผ JSON ๋ฐฐ์—ด๋กœ ๋ณ€ํ™˜
POSTGRES_QUERY_ESCAPED_OUTPUT=$(PGPASSWORD=$POSTGRES_PASSWORD psql \
  -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" \
  -d postgres -U "$POSTGRES_USERNAME" \
  -tAqX -P footer=off -c "$POSTGRES_QUERY" \
  | jq -cRs 'split("\n") | map(select(length > 0)) | @json')

# Terraform external data source ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ์ถœ๋ ฅ
echo "{\"string\": $POSTGRES_QUERY_ESCAPED_OUTPUT}"

์•„๋ž˜ ์ฝ”๋“œ๋Š” ํŠน์ • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋Œ€ํ•ด ์‚ฌ์šฉ์ž๋ณ„ ๋ณด์œ  ๊ถŒํ•œ ์ •๋ณด๋ฅผ ์กฐํšŒํ•˜๋Š” Bash ์Šคํฌ๋ฆฝํŠธ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

#!/bin/bash
# ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ์‹œ ์ฆ‰์‹œ ์ข…๋ฃŒ, ์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜ ์‚ฌ์šฉ์‹œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ
set -euo pipefail

# ์ฒซ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ›์€ PostgreSQL ์ ‘์† ์ •๋ณด JSON
POSTGRES_CREDENTIAL="$1"
# ๋‘ ๋ฒˆ์งธ ์ธ์ž๋กœ ๋ฐ›์€ ์กฐํšŒํ•  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ช…
POSTGRES_QUERY_DATABASE_NAME="$2"

# JSON์—์„œ PostgreSQL ์ ‘์† ์ •๋ณด ์ถ”์ถœ
POSTGRES_HOST=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.host')
POSTGRES_PORT=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.port')
POSTGRES_USERNAME=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.username')
POSTGRES_PASSWORD=$(echo "$POSTGRES_CREDENTIAL" | jq -r '.password')

# ํŠน์ • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ๋Œ€ํ•œ ์‚ฌ์šฉ์ž๋ณ„ ๊ถŒํ•œ ์ •๋ณด ์กฐํšŒ ์ฟผ๋ฆฌ
POSTGRES_QUERY=$(cat << EOF
WITH all_defined_roles AS (
  -- ์‹œ์Šคํ…œ ์—ญํ• ์„ ์ œ์™ธํ•œ ๋ชจ๋“  ์ •์˜๋œ ์—ญํ•  ์กฐํšŒ
  SELECT oid AS role_oid, rolname, rolcanlogin
  FROM pg_roles
  WHERE rolname NOT LIKE 'pg_%'
    AND rolname NOT LIKE 'rds%'
),
privilege_types_to_check AS (
  -- ํ™•์ธํ•  ๊ถŒํ•œ ์œ ํ˜•๋“ค (connect, create, temporary)
  VALUES ('connect'), ('create'), ('temporary')
),
target_database_info AS (
  -- ๋Œ€์ƒ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์˜ ๊ธฐ๋ณธ ์ •๋ณด ์กฐํšŒ
  SELECT oid AS db_oid, datdba AS db_owner_oid
  FROM pg_database
  WHERE datname = :'db'
),
effective_grantee_privileges AS (
  -- ๊ฐ ์—ญํ• ๋ณ„๋กœ ์‹ค์ œ ๋ณด์œ ํ•œ ๊ถŒํ•œ ์กฐํšŒ
  SELECT
    r.rolname AS grantee_name,
    r.rolcanlogin,
    r.role_oid AS grantee_oid,
    tdi.db_owner_oid,
    pt.column1 AS privilege_type
  FROM
    all_defined_roles r,
    privilege_types_to_check pt,
    target_database_info tdi
  WHERE
    EXISTS (SELECT 1 FROM target_database_info WHERE tdi.db_oid IS NOT NULL)
    AND has_database_privilege(r.role_oid, tdi.db_oid, pt.column1)
),
grouped_privileges AS (
  -- ์—ญํ• ๋ณ„๋กœ ๊ถŒํ•œ๋“ค์„ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ๋ฐฐ์—ด๋กœ ์ง‘๊ณ„
  SELECT
    grantee_name,
    rolcanlogin,
    grantee_oid,
    db_owner_oid,
    json_agg(privilege_type ORDER BY privilege_type) AS privileges_array
  FROM effective_grantee_privileges
  GROUP BY grantee_name, rolcanlogin, grantee_oid, db_owner_oid
)
-- ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ JSON ๊ฐ์ฒด ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ (์—ญํ• ๋ช…์„ ํ‚ค๋กœ, ๊ถŒํ•œ ์ •๋ณด๋ฅผ ๊ฐ’์œผ๋กœ)
SELECT COALESCE(
  json_object_agg(
    gp.grantee_name,
    json_build_object(
      'owner', (gp.grantee_oid = gp.db_owner_oid),
      'privileges', gp.privileges_array
    )
  ),
  '{}'::json
)
FROM grouped_privileges gp;
EOF
)

# ํฌํŠธ๊ฐ€ null์ธ ๊ฒฝ์šฐ ๊ธฐ๋ณธ๊ฐ’ 5432๋กœ ์„ค์ •
if [ "$POSTGRES_PORT" == "null" ] ; then
  POSTGRES_PORT="5432"
fi

# PostgreSQL ์ฟผ๋ฆฌ ์‹คํ–‰, ๊ฒฐ๊ณผ๋ฅผ ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ ํ›„ JSON ๋ฌธ์ž์—ด๋กœ ๋ณ€ํ™˜
POSTGRES_QUERY_ESCAPED_OUTPUT=$(PGPASSWORD=$POSTGRES_PASSWORD psql \
  -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" \
  -d postgres -U "$POSTGRES_USERNAME" -v db="$POSTGRES_QUERY_DATABASE_NAME" \
  -tAqX -P footer=off -c "$POSTGRES_QUERY" \
  | tr [:upper:] [:lower:] | jq -c '.' | jq -R '.')

# Terraform external data source ํ˜•์‹์œผ๋กœ ๊ฒฐ๊ณผ ์ถœ๋ ฅ
echo "{\"string\": $POSTGRES_QUERY_ESCAPED_OUTPUT}"

์•„๋ž˜ ์ฝ”๋“œ๋Š” ์•ž์˜ Bash ์Šคํฌ๋ฆฝํŠธ๋ฅผ Terraform external ๋ฐ์ดํ„ฐ ์†Œ์Šค๋กœ ํ˜ธ์ถœํ•˜์—ฌ
PostgreSQL ์‚ฌ์šฉ์ž ๋ชฉ๋ก์„ ๊ฐ€์ ธ์˜ค๋Š” ์ฝ”๋“œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

# PostgreSQL ์‚ฌ์šฉ์ž ๋ชฉ๋ก ์กฐํšŒ
data "external" "postgres_user_names" {
  program = [
    "bash", format("%s/bash/user_names.sh", path.module),
    jsonencode(var.credential)
  ]
}

# ๊ฐ ์‚ฌ์šฉ์ž๋ณ„ ํ• ๋‹น๋œ ์—ญํ•  ๋ชฉ๋ก ์กฐํšŒ
data "external" "postgres_user_assigned_role_names" {
  for_each = toset(jsondecode(data.external.postgres_user_names.result.string))
  program = [
    "bash", format("%s/bash/user_assigned_role_names.sh", path.module),
    jsonencode(var.credential), each.value
  ]
}

# PostgreSQL ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋ชฉ๋ก ์กฐํšŒ
data "external" "postgres_database_names" {
  program = [
    "bash", format("%s/bash/database_names.sh", path.module),
    jsonencode(var.credential)
  ]
}

# ๊ฐ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ณ„ ๊ถŒํ•œ ์ •๋ณด ์กฐํšŒ
data "external" "postgres_database_privileges" {
  for_each = toset(jsondecode(data.external.postgres_database_names.result.string))
  program = [
    "bash", format("%s/bash/database_privileges.sh", path.module),
    jsonencode(var.credential), each.value
  ]
}

์ด๋ ‡๊ฒŒ ํ•ด์„œ PostgreSQL ๊ณ„์ • ํ˜„ํ™ฉ๋„ Google Workspace, AWS์™€ ๋™์ผํ•œ ๊ธฐ์ค€์œผ๋กœ ๋ถ„๋ฅ˜ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

external ๋ฐ์ดํ„ฐ ์†Œ์Šค๋ฅผ ํ™œ์šฉํ•œ ๋ฐฉ์‹์ด ๋ณต์žกํ•ด ๋ณด์ผ ์ˆ˜ ์žˆ์ง€๋งŒ,
๊ฒฐ๊ณผ์ ์œผ๋กœ๋Š” ํ•˜๋‚˜์˜ ์ผ๊ด€๋œ ์ฒด๊ณ„ ์•ˆ์—์„œ ๋ชจ๋“  ๊ณ„์ • ์ •๋ณด๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฅผ ํ†ตํ•ด Google Workspace ๊ณ„์ •๊ณผ์˜ ์—ฐ๊ฒฐ ๊ณ ๋ฆฌ๋ฅผ ์‰ฝ๊ฒŒ ์ถ”์ ํ•  ์ˆ˜ ์žˆ์—ˆ๊ณ ,
๊ฐ ์‚ฌ์šฉ์ž๋ณ„๋กœ ํ• ๋‹น๋œ ์—ญํ• ๊ณผ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ณ„ ๊ถŒํ•œ ์ •๋ณด๋ฅผ ์ฒด๊ณ„์ ์œผ๋กœ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์ตœ์ข…, ์—ฐ๊ฒฐ ํ˜„ํ™ฉ์„ ํ•˜๋‚˜๋กœ ๋ฌถ๋‹ค

์ด์ œ๊นŒ์ง€ ์‚ดํŽด๋ณธ Google Workspace, AWS, PostgreSQL ๊ณ„์ • ์ •๋ณด๋ฅผ
์ตœ์ข…์ ์œผ๋กœ ํ•˜๋‚˜์˜ Terraform output์œผ๋กœ ์ •๋ฆฌํ•ด์•ผ๊ฒ ๋‹ค๊ณ  ํŒ๋‹จํ–ˆ์Šต๋‹ˆ๋‹ค.
terraform apply ๋ช…๋ น ํ•˜๋‚˜๋งŒ์œผ๋กœ ์ „์ฒด ๊ณ„์ • ์ •๋ณด๋ฅผ ํ™•์ธํ•  ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

์ด ๊ณผ์ •์—์„œ PostgreSQL ๊ณ„์ • ๋ชฉ๋ก์„ ์กฐํšŒํ•˜๋‹ค ๋ณด๋‹ˆ,
๊ณ„์ • ์ด๋ฆ„์— ํฌํ•จ๋œ .(dot)์„ _(underscore)๋กœ ๋ฐ”๊ฟ”์•ผ ํ•˜๋Š” ๊ทœ์น™์ด ์žˆ๋‹ค๋Š” ์ ์„ ํ™•์ธํ–ˆ๊ณ ,
์ด ๋ณ€ํ™˜ ๊ทœ์น™์„ output ๊ตฌ์„ฑ์—๋„ ๋ฐ˜์˜ํ–ˆ์Šต๋‹ˆ๋‹ค. (PostgreSQL์˜ ์‹๋ณ„์ž ์ œ์•ฝ)

๊ฒฐ๊ณผ์ ์œผ๋กœ ๋ชจ๋“  ๊ณ„์ •์„ ์‹ค์ œ ์‚ฌ์šฉ์ž์™€ ์‹œ์Šคํ…œ/์„œ๋น„์Šค์šฉ ๊ณ„์ •์œผ๋กœ ๋‚˜๋ˆ„๊ณ ,
ํ•œ ๋ฒˆ์˜ ์‹คํ–‰์œผ๋กœ ์ „์ฒด ๊ณ„์ • ํ˜„ํ™ฉ์„ ์ผ๊ด€๋˜๊ฒŒ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์กฐ๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

์•„๋ž˜ ์ฝ”๋“œ๋Š” ํ•ด๋‹น ๊ตฌ์กฐ๋ฅผ Terraform์œผ๋กœ ์ž‘์„ฑํ•œ ์˜ˆ์‹œ์ž…๋‹ˆ๋‹ค.

# ์‚ฌ์šฉ์ž ๊ณ„์ • ํ˜„ํ™ฉ ์ถœ๋ ฅ
output "users" {
  description = "์‚ฌ์šฉ์ž ๊ณ„์ • ๋ณ„ Google Workspace, AWS, PostgreSQL ํ˜„ํ™ฉ"
  value = {
    for email, primary_email in local.user_emails
    : email => {
      googleworkspace = {
        primary_email = data.googleworkspace_user.email[email].primary_email
        name = data.googleworkspace_user.email[email].name
        is_admin = data.googleworkspace_user.email[email].is_admin
        suspended = data.googleworkspace_user.email[email].suspended
        mfa_status = data.external.googleworkspace_user_mfa_status[email].result.string
      }
      
      aws = (
        contains(keys(data.aws_iam_user.user), email)
        ? {
          user_name = data.aws_iam_user.user[email].user_name
          arn = data.aws_iam_user.user[email].arn
          path = data.aws_iam_user.user[email].path
          user_id = data.aws_iam_user.user[email].user_id
        }
        : null
      )
      
      postgresql = (
        contains(
          jsondecode(data.external.postgres_user_names.result.string),
          replace(primary_email, ".", "_")
        )
        ? {
          user_name = replace(primary_email, ".", "_")
          assigned_roles = jsondecode(
            data.external.postgres_user_assigned_role_names[
              replace(primary_email, ".", "_")
            ].result.string
          )
        }
        : null
      )
    }
  }
}

# ์‹œ์Šคํ…œ/์„œ๋น„์Šค ๊ณ„์ • ํ˜„ํ™ฉ ์ถœ๋ ฅ
output "non_users" {
  description = "์‹œ์Šคํ…œ/์„œ๋น„์Šค ๊ณ„์ •๋ณ„ Google Workspace, AWS, PostgreSQL ์—ฐ๊ฒฐ ํ˜„ํ™ฉ"
  value = {
    for email, primary_email in local.non_user_emails
    : email => {
      googleworkspace = {
        primary_email = data.googleworkspace_user.email[email].primary_email
        name = data.googleworkspace_user.email[email].name
        is_admin = data.googleworkspace_user.email[email].is_admin
        suspended = data.googleworkspace_user.email[email].suspended
        mfa_status = data.external.googleworkspace_user_mfa_status[email].result.string
      }
      
      aws = (
        contains(keys(data.aws_iam_user.non_user), email)
        ? {
          user_name = data.aws_iam_user.non_user[email].user_name
          arn = data.aws_iam_user.non_user[email].arn
          path = data.aws_iam_user.non_user[email].path
          user_id = data.aws_iam_user.non_user[email].user_id
        }
        : null
      )
      
      postgresql = (
        contains(
          jsondecode(data.external.postgres_user_names.result.string),
          replace(primary_email, ".", "_")
        )
        ? {
          user_name = replace(primary_email, ".", "_")
          assigned_roles = jsondecode(
            data.external.postgres_user_assigned_role_names[
              replace(primary_email, ".", "_")
            ].result.string
          )
        }
        : null
      )
    }
  }
}

์ด๋ ‡๊ฒŒ ํ•ด์„œ ๊ณ„์ • ๊ด€๋ฆฌ ํ˜„ํ™ฉ ํŒŒ์•…์„ ์ž๋™ํ™”ํ•˜์—ฌ, ํ•œ๋ˆˆ์— ๋ณผ ์ˆ˜ ์žˆ๋Š” ๊ธฐ๋ฐ˜์„ ๋งˆ๋ จํ•  ์ˆ˜ ์žˆ์—ˆ์Šต๋‹ˆ๋‹ค.

๋งˆ๋ฌด๋ฆฌ

์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ๋Š” ์ด๋ฒˆ ๊ธ€์— ๋‹ด์ง€ ๋ชปํ•œ ๊ถŒํ•œ, ์—ญํ•  ๋“ฑ ๋” ๋ณต์žกํ•œ ์š”์†Œ๋“ค์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ์ „์ฒด ๊ตฌ์กฐ๋ฅผ ๋ชจ๋‘ ์„ค๋ช…ํ•˜๊ธฐ๋ณด๋‹ค๋Š”,
๊ณ„์ • ํ˜„ํ™ฉ์„ ์ค‘์‹ฌ์œผ๋กœ ํ•ต์‹ฌ์ ์ธ ํ๋ฆ„๋งŒ ๊ฐ„๊ฒฐํ•˜๊ฒŒ ์ •๋ฆฌํ•˜๋Š” ๋ฐ ์ง‘์ค‘ํ–ˆ์Šต๋‹ˆ๋‹ค.

๋˜ํ•œ ์—ฌ๊ธฐ์„œ ๋ชจ๋“  ์ผ์ด ๋๋‚œ ๊ฒƒ์€ ์•„๋‹™๋‹ˆ๋‹ค.

์ด์ œ ๋ง‰ ํ˜„ํ™ฉ์„ ํŒŒ์•…ํ•  ์ˆ˜ ์žˆ๋Š” ์ถœ๋ฐœ์ ์„ ๋งŒ๋“  ๊ฒƒ์— ๋ถˆ๊ณผํ•˜๋ฉฐ,
์•ž์œผ๋กœ๋Š” ์ด ๊ธฐ๋ฐ˜ ์œ„์— ์ •์ฑ…์„ ์ •๋น„ํ•˜๊ณ , ์ž๋™ํ™”๋ฅผ ๊ฐ•ํ™”ํ•˜๋ฉฐ,
์„œ๋น„์Šค์™€ ์กฐ์ง์ด ์„ฑ์žฅํ•ด๋„ ํ”๋“ค๋ฆฌ์ง€ ์•Š๋Š” ์šด์˜ ์ฒด๊ณ„๋ฅผ ๋งŒ๋“ค์–ด๊ฐ€์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์ด๋ฒˆ ๊ธ€์—์„œ๋Š” ๊ทธ ์‹œ์ž‘์ ์œผ๋กœ์„œ,
๊ณ„์ • ๊ด€๋ฆฌ ํ˜„ํ™ฉ์„ ์–ด๋–ป๊ฒŒ ํŒŒ์•…ํ•˜๊ณ  ์ •๋ฆฌํ–ˆ๋Š”์ง€์— ๋Œ€ํ•œ ๋‚ด์šฉ์„ ๊ณต์œ ๋“œ๋ ธ์Šต๋‹ˆ๋‹ค.

๋„คํŠธ์›Œํฌ ๊ตฌ์„ฑ๊ณผ ๋น„์šฉ ๊ด€๋ฆฌ์— ๊ด€ํ•œ ์ด์•ผ๊ธฐ๋Š” ๋‹ค์Œ ๊ธ€์—์„œ ๋” ์ž์„ธํžˆ ์ด์–ด๊ฐ€๊ฒ ์Šต๋‹ˆ๋‹ค.

๊ธด ๊ธ€ ๋๊นŒ์ง€ ์ฝ์–ด์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค.

โ† ๋ชฉ๋ก์œผ๋กœ ๋Œ์•„๊ฐ€๊ธฐ

Art Changes Life

๋…ธ๋จธ์Šค์™€ ํ•จ๊ป˜ ์—”ํ„ฐํ…Œํฌ ์‚ฐ์—…์„ ํ˜์‹ ํ•ด๋‚˜๊ฐˆ ๋ฉค๋ฒ„๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.

์ฑ„์šฉ ์ค‘์ธ ๊ณต๊ณ  ๋ณด๊ธฐ