
์ฒซ ์ธํ๋ผ ๋ด๋น์๋ก ์ด์๋จ๊ธฐ: ์์, ๊ณํ, ๊ณ์ ๊ด๋ฆฌ

- #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์ 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์ ํ์ฉํ์ต๋๋ค.
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์ผ๋ก ํํฉ์ ํ์ธํ๋ ์ผ์ ๊ทธ๋ฆฌ ์ด๋ ต์ง ์์์ต๋๋ค.
๋ค๋ง, ๋น์ ์ ๋ ์
์ฌํ ์ง 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
)
}
}
}
์ด๋ ๊ฒ ํด์ ๊ณ์ ๊ด๋ฆฌ ํํฉ ํ์ ์ ์๋ํํ์ฌ, ํ๋์ ๋ณผ ์ ์๋ ๊ธฐ๋ฐ์ ๋ง๋ จํ ์ ์์์ต๋๋ค.
๋ง๋ฌด๋ฆฌ
์ค์ ํ๋ก์ ํธ์์๋ ์ด๋ฒ ๊ธ์ ๋ด์ง ๋ชปํ ๊ถํ, ์ญํ ๋ฑ ๋ ๋ณต์กํ ์์๋ค์ด ํฌํจ๋์ด ์์ต๋๋ค.
์ด๋ฒ ๊ธ์์๋ ์ ์ฒด ๊ตฌ์กฐ๋ฅผ ๋ชจ๋ ์ค๋ช
ํ๊ธฐ๋ณด๋ค๋,
๊ณ์ ํํฉ์ ์ค์ฌ์ผ๋ก ํต์ฌ์ ์ธ ํ๋ฆ๋ง ๊ฐ๊ฒฐํ๊ฒ ์ ๋ฆฌํ๋ ๋ฐ ์ง์คํ์ต๋๋ค.
๋ํ ์ฌ๊ธฐ์ ๋ชจ๋ ์ผ์ด ๋๋ ๊ฒ์ ์๋๋๋ค.
์ด์ ๋ง ํํฉ์ ํ์
ํ ์ ์๋ ์ถ๋ฐ์ ์ ๋ง๋ ๊ฒ์ ๋ถ๊ณผํ๋ฉฐ,
์์ผ๋ก๋ ์ด ๊ธฐ๋ฐ ์์ ์ ์ฑ
์ ์ ๋นํ๊ณ , ์๋ํ๋ฅผ ๊ฐํํ๋ฉฐ,
์๋น์ค์ ์กฐ์ง์ด ์ฑ์ฅํด๋ ํ๋ค๋ฆฌ์ง ์๋ ์ด์ ์ฒด๊ณ๋ฅผ ๋ง๋ค์ด๊ฐ์ผ ํฉ๋๋ค.
์ด๋ฒ ๊ธ์์๋ ๊ทธ ์์์ ์ผ๋ก์,
๊ณ์ ๊ด๋ฆฌ ํํฉ์ ์ด๋ป๊ฒ ํ์
ํ๊ณ ์ ๋ฆฌํ๋์ง์ ๋ํ ๋ด์ฉ์ ๊ณต์ ๋๋ ธ์ต๋๋ค.
๋คํธ์ํฌ ๊ตฌ์ฑ๊ณผ ๋น์ฉ ๊ด๋ฆฌ์ ๊ดํ ์ด์ผ๊ธฐ๋ ๋ค์ ๊ธ์์ ๋ ์์ธํ ์ด์ด๊ฐ๊ฒ ์ต๋๋ค.
๊ธด ๊ธ ๋๊น์ง ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌํฉ๋๋ค.