Post

Terraform Study #4

โ€˜ํ…Œ๋ผํผ์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š” IaCโ€™ ์ฑ…์œผ๋กœ ์ง„ํ–‰ํ•˜๋Š” Terraform ์Šคํ„ฐ๋””[T101] 4์ฃผ์ฐจ ์ •๋ฆฌ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค.

4์ฃผ์ฐจ

์ด๋ฒˆ ์ฃผ์ฐจ์—์„œ๋Š” ๊ธฐ๋ณธ ๋ฌธ๋ฒ•์„ ๋„˜์–ด, ์ฝ”๋“œ๋ฅผ ๊ตฌ์กฐํ™”ํ•˜๊ณ  ํ˜‘์—…ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๋ฐฐ์šด๋‹ค. ๊ตฌ์ฒด์ ์œผ๋กœ๋Š” module๊ณผ state์— ๋Œ€ํ•ด ํ•™์Šตํ•˜๋ฉฐ, ํ˜‘์—…๊ณผ ๊ด€๋ จ๋œ ๋‚ด์šฉ์€ 5์ฃผ์ฐจ์—์„œ ๋” ์ž์„ธํ•˜๊ฒŒ ๋‹ค๋ฃฌ๋‹ค.

State

์•„๋ž˜๋Š” 1์ฃผ์ฐจ ์ •๋ฆฌ๋‚ด์šฉ์ด๋‹ค. ํ…Œ๋ผํผ์—์„œ๋Š” State ํŒŒ์ผ์„ Serial์„ ๊ธฐ์ค€์œผ๋กœ backup ๊ด€๋ฆฌํ•œ๋‹ค.

Terraform์˜ .tfstate ํŒŒ์ผ ๋‚ด์˜ serial ๊ฐ’์€ ์ƒํƒœ ํŒŒ์ผ์˜ ๋ฒ„์ „์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ๋™์‹œ์„ฑ ์ œ์–ด์™€ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ํ™•์ธ์—๋„ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•œ๋‹ค. ์ด ๊ฐ’์€ Terraform ๋ช…๋ น์ด ์‹คํ–‰๋  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ฆ๊ฐ€ํ•˜์—ฌ, ์ƒํƒœ ํŒŒ์ผ์˜ ์ตœ์‹ ์„ฑ๊ณผ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•œ๋‹ค. ๊ทธ๋ ‡๊ธฐ์— backup ํŒŒ์ผ์ด ํ˜„์žฌ state ํŒŒ์ผ๋ณด๋‹ค serial ๋ฒˆํ˜ธ๊ฐ€ ๋‚ฎ๋‹ค.

์ด๋ก ์ ์ธ ๋‚ด์šฉ

์ƒํƒœ ํŒŒ์ผ์€ ๋ฐฐํฌํ•  ๋•Œ๋งˆ๋‹ค ๋ณ€๊ฒฝ๋˜๋Š” ํ”„๋ผ์ด๋น— API์ด๋ฉฐ, ์˜ค์ง ํ…Œ๋ผํผ ๋‚ด๋ถ€์—์„œ ์‚ฌ์šฉ์šฉ๋„์ด๋‹ˆ ์ง์ ‘ ํŽธ์ง‘ํ•˜๊ฑฐ๋‚˜ ์ž‘์„ฑํ•ด์„œ๋Š” ์•ˆ๋œ๋‹ค. (ํŒŒ์ผ ๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ํ†ตํ•ด, API๋ฅผ ์š”์ฒญํ•˜๋Š” ๊ฒƒ ๊ฐ™๋‹ค.)

๋งŒ์•ฝ, ํ…Œ๋ผํผ์„ ํ†ตํ•ด ํ˜‘์—…์„ ์ง„ํ–‰ํ•ด์•ผ ํ•œ๋‹ค๋ฉด state ํŒŒ์ผ์„ ๊ด€๋ฆฌํ•ด์•ผ ํ•œ๋‹ค. ์ด๋•Œ๋Š” 1์ฃผ์ฐจ๋•Œ ์ง„ํ–‰ํ–ˆ๋˜ ์›๊ฒฉ ๋ฐฑ์—”๋“œ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

ํŒ€ ๋‹จ์œ„ ์šด์˜์‹œ ํ•„์š”ํ•œ ์ ์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

  • state ํŒŒ์ผ์˜ ๊ณต์œ  ์Šคํ† ๋ฆฌ์ง€
  • Locking(ํ•œ๋ช…์— ํ•œ๋ช…์”ฉ)
  • ํŒŒ์ผ ๊ฒฉ๋ฆฌ(dev, stage ๋“ฑ ํ™˜๊ฒฝ ๋ณ„ ๊ฒฉ๋ฆฌ๊ฐ€ ํ•„์š”)

์•„๋ž˜๋Š” VCS๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ, ๋ฐœ์ƒํ•˜๋Š” ๋ฌธ์ œ์ ์ด๋‹ค.

  • VCS: ์ˆ˜๋™์œผ๋กœ ์ƒํƒœํŒŒ์ผ์„ push, pull ํ•ด์•ผ ํ•˜๋‹ˆ ํœด๋จผ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋‹ค.++ Lock ๊ธฐ๋Šฅ์ด ์—†๋‹ค.

๊ฒฐ๊ตญ, ํ…Œ๋ผํผ์„ ์ง€์›ํ•˜๋Š” ์›๊ฒฉ ๋ฐฑ์—”๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. S3, Terraform Cloud ๋“ฑ์ด ์žˆ๋‹ค.

๊ด€๋ จ ์‹ค์Šต

State ํŒŒ์ผ์—๋Š” ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ๋ชจ๋“  ๊ฒƒ์ด ๋‹ด๊ฒจ์žˆ๋‹ค. ์•„๋ž˜์˜ ์‹ค์Šต์„ ํ†ตํ•ด, ์ƒํƒœ ํŒŒ์ผ ๊ด€๋ฆฌ์˜ ์ค‘์š”์„ฑ์„ ํ™•์ธํ•ด๋ณธ๋‹ค.

  • ํŒจ์Šค์›Œ๋“œ ๋ฆฌ์†Œ์Šค ์ฝ”๋“œ
1
2
3
4
5
resource "random_password" "mypw" {
  length           = 16
  special          = true
  override_special = "!#$%"
}

์•„๋ž˜์˜ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ๋ฆฌ์†Œ์Šค ์ƒ์„ฑ

1
terraform init && terraform plan && terraform apply -auto-approve

ํ…Œ๋ผํผ ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ๋ฆฌ์†Œ์Šค๋ฅผ ํ™•์ธํ•˜๋ฉด, ์‹œํฌ๋ฆฟ ์ •๋ณด๋Š” ์•Œ๋ ค์ฃผ์ง€ ์•Š๋Š”๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
โฏ terraform state show random_password.mypw

# random_password.mypw:
resource "random_password" "mypw" {
    bcrypt_hash      = (sensitive value)
    id               = "none"
    length           = 16
    lower            = true
    min_lower        = 0
    min_numeric      = 0
    min_special      = 0
    min_upper        = 0
    number           = true
    numeric          = true
    override_special = "!#$%"
    result           = (sensitive value)
    special          = true
    upper            = true
}

ํ•˜์ง€๋งŒ, state ํŒŒ์ผ์„ ํ™•์ธํ•ด๋ณด๋ฉด ๋ฆฌ์†Œ์Šค์— ๋Œ€ํ•œ ์ •๋ณด๊ฐ€ ์ „๋ถ€ ์กด์žฌํ•œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
  ...
  "resources": [
    {
      "mode": "managed",
      "type": "random_password",
     ...
      "instances": [
        {
          "schema_version": 3,
          "attributes": {
            "bcrypt_hash": "$2a$10$pLMnmRKSY52ageoVumPlpuP5dyo2GZpomOxo6MsQetO/F28dR2ge2",
            "id": "none",
          ...
            "result": "CLTOsYB9zWifY9WT",
            "special": true,
            "upper": true
          },
          "sensitive_attributes": []
        }
      ]
    }
  ],
  "check_results": null
}

ํ…Œ๋ผํผ ์ฝ˜์†”์—์„œ๋„ sensitive value๋Š” ๋ณด์ด์ง€ ์•Š๋Š”๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
echo "random_password.mypw" | terraform console
{
  "bcrypt_hash" = (sensitive value)
  "id" = "none"
  "keepers" = tomap(null) /* of string */
  "length" = 16
  ...
  "override_special" = "!#$%"
  "result" = (sensitive value)
  "special" = true
  "upper" = true
}

.tfstate

Terraform์˜ .tfstate ํŒŒ์ผ ๋‚ด์˜ serial ๊ฐ’์€ ์ƒํƒœ ํŒŒ์ผ์˜ ๋ฒ„์ „์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ๋™์‹œ์„ฑ ์ œ์–ด์™€ ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ํ™•์ธ์—๋„ ์ค‘์š”ํ•œ ์—ญํ• ์„ ํ•œ๋‹ค. ์ด ๊ฐ’์€ Terraform ๋ช…๋ น์ด ์‹คํ–‰๋  ๋•Œ๋งˆ๋‹ค ์ž๋™์œผ๋กœ ์ฆ๊ฐ€ํ•˜์—ฌ, ์ƒํƒœ ํŒŒ์ผ์˜ ์ตœ์‹ ์„ฑ๊ณผ ์ผ๊ด€์„ฑ์„ ์œ ์ง€ํ•œ๋‹ค. ๊ทธ๋ ‡๊ธฐ์— backup ํŒŒ์ผ์ด ํ˜„์žฌ state ํŒŒ์ผ๋ณด๋‹ค serial ๋ฒˆํ˜ธ๊ฐ€ ๋‚ฎ๋‹ค.

์œ ํ˜•๊ตฌ์„ฑ ๋ฆฌ์†Œ์Šค ์ •์˜State ๊ตฌ์„ฑ ๋ฐ์ดํ„ฐ์‹ค์ œ ๋ฆฌ์†Œ์Šค๊ธฐ๋ณธ ์˜ˆ์ƒ ๋™์ž‘
1์žˆ์Œย ย ๋ฆฌ์†Œ์Šค ์ƒ์„ฑ
2์žˆ์Œ์žˆ์Œย ๋ฆฌ์†Œ์Šค ์ƒ์„ฑ
3์žˆ์Œ์žˆ์Œ์žˆ์Œ๋™์ž‘ ์—†์Œ
4ย ์žˆ์Œ์žˆ์Œ๋ฆฌ์†Œ์Šค ์‚ญ์ œ
5ย ย ์žˆ์Œ๋™์ž‘ ์—†์Œ
6์žˆ์Œย ์žˆ์Œย 
  • -refresh=false ์˜ต์…˜ ํ˜„์žฌ์˜ stateํŒŒ์ผ๊ณผ ํ…Œ๋ผํผ์ฝ”๋“œ๋ฅผ ๋น„๊ตํ•˜์—ฌ ๊ทธ๋Œ€๋กœ ์ ์šฉํ•œ๋‹ค. ์›๊ฒฉ ๋ฆฌ์†Œ์Šค์˜ ์‹ค์ œ ์ƒํƒœ๋Š” ํ™•์ธํ•˜์ง€ ์•Š์œผ๋ฉฐ ๋งŒ์•ฝ ์›๊ฒฉ๋ฆฌ์†Œ์Šค๊ฐ€ ์ œ๊ฑฐ๋˜์—ˆ์–ด๋„ ๋‹ค์‹œ ์ƒ์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค.

์œ ํ˜•6๋ฒˆ ์‹ค์Šต์ง„ํ–‰

  • IAM user๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๋ฆฌ์†Œ์Šค ํ™•์ธ
1
2
3
4
5
6
7
8
9
10
11
locals {
  name = "mytest"
}

resource "aws_iam_user" "myiamuser1" {
  name = "${local.name}1"
}

resource "aws_iam_user" "myiamuser2" {
  name = "${local.name}2"
}
  • ๋ฐฐํฌ์ง„ํ–‰
1
terraform apply -auto-approve
  • ๋ฐฐํฌ ์ƒํƒœ ํ™•์ธ
1
2
3
4
aws iam list-users | jq '.Users[] | .UserName'
"admin"
"mytest1"
"mytest2"
  • tfstate ํŒŒ์ผ ์‚ญ์ œ
1
2
3
rm -rf terraform.tfstate*
โฏ ls terraform.tfstate*
zsh: no matches found: terraform.tfstate*
  • Plan ๋ช…๋ น์„ ์‹คํ–‰ํ•˜๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ๋ฆฌ์†Œ์Šค๋ฅผ ํŒŒ์•…ํ•˜์ง€ ๋ชปํ•˜๊ณ  ์ƒˆ๋กญ๊ฒŒ ์ƒ์„ฑํ•˜๋ ค๊ณ  ํ•จ.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ terraform plan

Terraform used the selected providers to generate the following execution plan. Resource
actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_iam_user.myiamuser1 will be created
  + resource "aws_iam_user" "myiamuser1" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "mytest1"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }
  • apply ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด, EntityAlreadyExists ์—๋Ÿฌ ๋ฐœ์ƒ
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ terraform apply
...
Plan: 2 to add, 0 to change, 0 to destroy.
aws_iam_user.myiamuser2: Creating...
aws_iam_user.myiamuser1: Creating...
โ•ท
โ”‚ Error: creating IAM User (mytest1): **EntityAlreadyExists: User with name mytest1 already exists.**
โ”‚       status code: 409, request id: e32ae858-e9eb-4c3a-a6ab-d7dba9f8bbd8
โ”‚ 
โ”‚   with aws_iam_user.myiamuser1,
โ”‚   on main.tf line 5, in resource "aws_iam_user" "myiamuser1":
โ”‚    5: resource "aws_iam_user" "myiamuser1" {
โ”‚ 
โ•ต
  • ์ด๋Ÿด๋•Œ๋Š” import ๋ช…๋ น์–ด๋ฅผ ํ†ตํ•ด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค, IAM user์˜ ID๋Š” ์œ ์ €์˜ ์ด๋ฆ„์ด๋ฏ€๋กœ ์•„๋ž˜์˜ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.
    • terraform import [options] ADDRESS ID
1
2
3
4
5
6
7
8
9
10
terraform import aws_iam_user.myiamuser1 mytest1                     
aws_iam_user.myiamuser1: Importing from ID "mytest1"...
aws_iam_user.myiamuser1: Import prepared!
  Prepared aws_iam_user for import
aws_iam_user.myiamuser1: Refreshing state... [id=mytest1]

Import successful!

The resources that were imported are shown above. These resources are now in
your Terraform state and will henceforth be managed by Terraform.
  • tfstate ํ™•์ธ, myiamuser1์˜ ์ƒํƒœํŒŒ์ผ์ด ์ถ”๊ฐ€๋˜์—ˆ๋‹ค.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
  "version": 4,
  "terraform_version": "1.5.6",
  "serial": 4,
  "lineage": "0e52f56e-fe1e-d6e7-acb3-f521a3c2f365",
  "outputs": {},
  "resources": [
    {
      "mode": "managed",
      "type": "aws_iam_user",
      "name": "myiamuser1",
      "provider": "provider[\"registry.terraform.io/hashicorp/aws\"]",
      "instances": [
        {
          "schema_version": 0,
          "attributes": {
						...
            "id": "mytest1",
            "name": "mytest1",
            
          },
          "sensitive_attributes": [],
...
}

์›Œํฌ์ŠคํŽ˜์ด์Šค

State๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ๋…ผ๋ฆฌ์ ์ธ ๊ฐ€์ƒ ๊ณต๊ฐ„์„ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ผ๊ณ ํ•œ๋‹ค.

๊ฐœ๋ฐœ์šฉ ํ™˜๊ฒฝ, ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ, ์šด์˜ํ™˜๊ฒฝ์€ ๋Œ€๋ถ€๋ถ„ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค. ๊ฑฐ์˜ ์œ ์‚ฌํ•œ ํ™˜๊ฒฝ์„ ๊ตฌ์ถ•ํ•œ๋‹ค๊ณ  ํ•˜๋ฉด ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํ”„๋กœ์ ํŠธ๋ฅผ ํ†ตํ•ด ์šด์˜ํ•  ์ˆ˜ ์žˆ์ง€๋งŒ ๋™์ผํ•œ ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•œ๋‹ค๋ฉด ์ด๋Š” ์ผ๊ด€์„ฑ ์œ ์ง€์— ์ข‹์ง€ ์•Š๋‹ค. ํ…Œ๋ผํผ์—์„œ๋Š” ์ด๋ฅผ ์œ„ํ•ด workspace๋ฅผ ์ง€์›ํ•œ๋‹ค. ํ•˜๋‚˜์˜ ์ฝ”๋“œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํ™˜๊ฒฝ์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ terraform.workspace ๋ณ€์ˆ˜๋ฅผ ํ†ตํ•ด ํ™˜๊ฒฝ๋งˆ๋‹ค ๋ฆฌ์†Œ์Šค๋ฅผ ์กฐ์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

vs ์•„๋ž˜๋Š” ์›Œํฌ์ŠคํŽ˜์ด์Šค๊ฐ€ ์•„๋‹Œ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ํ”„๋กœ์ ํŠธ๋กœ ๊ตฌ์„ฑํ•œ ๋ชจ์Šต์ด๋‹ค.

[์–ธ๊ธ‰ํ•ด์ฃผ์‹  ์›Œํฌ์ŠคํŽ˜์ด์Šค์˜ ์žฅ๋‹จ์ ]

[์žฅ์ ]

  • ํ•˜๋‚˜์˜ ๋ฃจํŠธ ๋ชจ๋“ˆ์—์„œ ๋‹ค๋ฅธ ํ™˜๊ฒฝ์„ ์œ„ํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ๋™์ผํ•œ ํ…Œ๋ผํผ ๊ตฌ์„ฑ์œผ๋กœ ํ”„๋กœ๋น„์ €๋‹ํ•˜๊ณ  ๊ด€๋ฆฌ
  • ๊ธฐ์กด ํ”„๋กœ๋น„์ €๋‹๋œ ํ™˜๊ฒฝ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๊ณ  ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์‹คํ—˜ ๊ฐ€๋Šฅ
  • ๊นƒ์˜ ๋ธŒ๋žœ์น˜ ์ „๋žต์ฒ˜๋Ÿผ ๋™์ผํ•œ ๊ตฌ์„ฑ์—์„œ ์„œ๋กœ ๋‹ค๋ฅธ ๋ฆฌ์†Œ์Šค ๊ฒฐ๊ณผ ๊ด€๋ฆฌ - [์ฐธ๊ณ  : ํ™”ํ•ด - Git ๋ธŒ๋žœ์น˜ ์ „๋žต ์ˆ˜๋ฆฝ์„ ์œ„ํ•œ ์ „๋ฌธ๊ฐ€์˜ ์กฐ์–ธ๋“ค]

[๋‹จ์ ]

  • State๊ฐ€ ๋™์ผํ•œ ์ €์žฅ์†Œ(๋กœ์ปฌ ๋˜๋Š” ๋ฐฑ์—”๋“œ)์— ์ €์žฅ๋˜์–ด State ์ ‘๊ทผ ๊ถŒํ•œ ๊ด€๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅ(์–ด๋ ค์›€)
  • ๋ชจ๋“  ํ™˜๊ฒฝ์ด ๋™์ผํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ์š”๊ตฌํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํ…Œ๋ผํผ ๊ตฌ์„ฑ์— ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ๊ฐ€ ๋‹ค์ˆ˜ ๋ฐœ์ƒ ๊ฐ€๋Šฅ
  • ํ”„๋กœ๋น„์ €๋‹ ๋Œ€์ƒ์— ๋Œ€ํ•œ ์ธ์ฆ ์š”์†Œ๋ฅผ ์™„๋ฒฝํžˆ ๋ถ„๋ฆฌํ•˜๊ธฐ ์–ด๋ ค์›€
  • ๊ฐ€์žฅ ํฐ ๋‹จ์ ์€ ์™„๋ฒฝํ•œ ๊ฒฉ๋ฆฌ๊ฐ€ ๋ถˆ๊ฐ€๋Šฅ
    • โ‡’ ํ•ด๊ฒฐ๋ฐฉ์•ˆ 1. ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ๋ฃจํŠธ ๋ชจ๋“ˆ์„ ๋ณ„๋„๋กœ ๊ตฌ์„ฑํ•˜๋Š” ๋””๋ ‰ํ„ฐ๋ฆฌ ๊ธฐ๋ฐ˜์˜ ๋ ˆ์ด์•„์›ƒ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.
    • โ‡’ ํ•ด๊ฒฐ๋ฐฉ์•ˆ 2. Terraform Cloud ํ™˜๊ฒฝ์˜ ์›Œํฌ์ŠคํŽ˜์ด์Šค๋ฅผ ํ™œ์šฉ

Module

๋ชจ๋“ˆ์€ ๋Œ€๋ถ€๋ถ„์˜ ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์—์„œ ์“ฐ์ด๋Š” ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋‚˜ ํŒจํ‚ค์ง€์™€ ์—ญํ• ์ด ๋น„์Šทํ•˜๋‹ค.

์ค‘๋ณต๋˜๊ฑฐ๋‚˜, ์ž์ฃผ์“ฐ๋Š” ์ฝ”๋“œ๋ฅผ ๋ชจ๋“ˆํ™”ํ•ด์„œ ํŽธํ•˜๊ฒŒ ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

๋ชจ๋“ˆ ๋””๋ ‰ํ„ฐ๋ฆฌ ํ˜•์‹์€ terraform-<ํ”„๋กœ๋ฐ”์ด๋” ์ด๋ฆ„>-<๋ชจ๋“ˆ ์ด๋ฆ„> ํ˜•์‹์„ ์ œ์•ˆํ•œ๋‹ค.

์ด ํ˜•์‹์€ Terraform Cloud, Terraform Enterprise์—์„œ๋„ ์‚ฌ์šฉ๋˜๋Š” ๋ฐฉ์‹์œผ๋กœ 1) ๋””๋ ‰ํ„ฐ๋ฆฌ ๋˜๋Š” ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ด๋ฆ„์ด ํ…Œ๋ผํผ์„ ์œ„ํ•œ ๊ฒƒ์ด๊ณ , 2) ์–ด๋–ค ํ”„๋กœ๋ฐ”์ด๋”์˜ ๋ฆฌ์†Œ์Šค๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ์œผ๋ฉฐ, 3) ๋ถ€์—ฌ๋œ ์ด๋ฆ„์ด ๋ฌด์—‡์ธ์ง€ ํŒ๋ณ„ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•œ๋‹ค.

๊ตฌ์กฐ

์•„๋ž˜์™€ ๊ฐ™์ด ๋ฃจํŠธ ๋ชจ๋“ˆ์—์„œ ์ž์‹ ๋ชจ๋“ˆ์„ ์ฐธ์กฐํ•œ๋‹ค. ์ž์‹๋ชจ๋“ˆ์ด ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜๊ณ , ๋ฃจํŠธ ๋ชจ๋“ˆ์ด main ํ•จ์ˆ˜์ด๋‹ค. ์ž์‹ ๋ชจ๋“ˆ์„ ํ˜ธ์ถœํ•  ๋•Œ, ๋ณ€์ˆ˜๋„ ๋งž๊ฒŒ ๋Œ€์ž…ํ•œ๋‹ค.

image

์ถœ์ฒ˜: https://jloudon.com/cloud/Azure-Policy-as-Code-with-Terraform-Part-1/

๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ๋ฅผ ํ™•์ธํ•ด๋ณด๋ฉด, ์•„๋ž˜์˜ ์ž์‹ ๋ชจ๋“ˆ์„ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ณด์ž. isDB๋ผ๋Š” ๋ณ€์ˆ˜์˜ ๊ฐ’์„ ๋Œ€์ž…ํ•ด์ค˜์•ผํ•˜๊ณ , id์™€ pw๋ฅผ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# main.tf
resource "random_pet" "name" {
  keepers = {
    ami_id = timestamp()
  }
}

# DB์ผ ๊ฒฝ์šฐ Password ์ƒ์„ฑ ๊ทœ์น™์„ ๋‹ค๋ฅด๊ฒŒ ๋ฐ˜์˜ 
resource "random_password" "password" {
  length           = var.isDB ? 16 : 10
  special          = var.isDB ? true : false
  override_special = "!#$%*?"
}
# variable.tf
variable "isDB" {
  type        = bool
  default     = false
  description = "ํŒจ์Šค์›Œ๋“œ ๋Œ€์ƒ์˜ DB ์—ฌ๋ถ€"
}
# output.tf
output "id" {
  value = random_pet.name.id
}

output "pw" {
  value = nonsensitive(random_password.password.result) 
}

๋ฃจํŠธ ๋ชจ๋“ˆ

โ€œmypw1โ€์˜ ๊ฒฝ์šฐ๋Š” ๋ณ€์ˆ˜๋ฅผ ๋Œ€์ž…ํ•˜์ง€ ์•Š์•˜์œผ๋‹ˆ, isDB์˜ ๊ธฐ๋ณธ๊ฐ’์ด ๋“ค์–ด๊ฐ„๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๋ชจ๋“ˆ์„ ํ†ตํ•ด ์ฝ”๋“œ๋ฅผ ๊ตฌ์กฐํ™”ํ•˜๊ณ  ์žฌ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module "mypw1" {
  source = "../modules/terraform-random-pwgen"
}

module "mypw2" {
  source = "../modules/terraform-random-pwgen"
  isDB   = true
}

output "mypw1" {
  value  = module.mypw1
}

output "mypw2" {
  value  = module.mypw2
}

ํ”„๋กœ๋ฐ”์ด๋” ์ •์˜

๋ฃจํŠธ๋ชจ๋“ˆ์—์„œ ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ์ •์˜ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค. ๋งŒ์•ฝ, ์ž์‹๋ชจ๋“ˆ์—์„œ ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ์ •์˜ํ•˜๋ฉด ๋ฃจํŠธ ๋ชจ๋“ˆ์— ๋ฒ„์ „์ด ๋‹ค๋ฅด๋ฉด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  ๋ชจ๋“ˆ์— ๋ฐ˜๋ชฉ๋ฌธ์„ ์“ธ ์ˆ˜ ์—†๋‹ค.

์•„๋ž˜์˜ module โ€œexampleโ€๊ณผ ๊ฐ™์ด ๋ฃจํŠธ ๋ชจ๋“ˆ์— ํ”„๋กœ๋ฐ”์ด๋”๋ฅผ ์„ ์–ธํ•œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# The default "aws" configuration is used for AWS resources in the root
# module where no explicit provider instance is selected.
provider "aws" {
  region = "us-west-1"
}

# An alternate configuration is also defined for a different
# region, using the alias "usw2".
provider "aws" {
  alias  = "usw2"
  region = "us-west-2"
}

# An example child module is instantiated with the alternate configuration,
# so any AWS resources it defines will use the us-west-2 region.
module "example" {
  source    = "./example"
  providers = {
    aws = aws.usw2
  }
}

๋ชจ๋“ˆ์€ ์œ„์—์„œ ์ง„ํ–‰ํ•˜๋“ฏ์ด ๋กœ์ปฌํŒŒ์ผ๋กœ ๊ฐ€๋Šฅํ•˜๋ฉฐ ํ…Œ๋ผํผ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ, ๊นƒํ—ˆ๋ธŒ ๋“ฑ์—์„œ ๊ฐ€์ ธ์™€์„œ ์“ธ ์ˆ˜ ์žˆ๋‹ค.

ex) Terraform registry์—์„œ ๊ฐ€์ ธ์˜ค๊ธฐ

1
2
3
4
module "consul" {
  source = "hashicorp/consul/aws"
  version = "0.1.0"
}

ํ˜‘์—…

S3๋ฅผ ํ†ตํ•ด, ๋ฐฑ์—”๋“œ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ฒƒ์€ 1์ฃผ์ฐจ๋•Œ ์ง„ํ–‰ํ–ˆ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” Terraform Cloud๋ฅผ ์ด์šฉํ•˜์—ฌ TFC ๋ฐฑ์—”๋“œ๋ฅผ ๊ตฌ์„ฑํ•ด๋ณธ๋‹ค. ๋‹น์—ฐํžˆ Lock ๊ธฐ๋Šฅ๊ณผ ๋ฒ„์ „๊ด€๋ฆฌ๋„ ์ง€์›ํ•œ๋‹ค.

Terraform Cloud

state ๊ด€๋ฆฌ๋ฅผ ์ง„ํ–‰ํ•˜๋Š” TFC๋Š” ๋ฌด์ƒ์ด๋ผ๊ณ  ํ•œ๋‹ค.

[TFC]

  • ์ œ๊ณต ๊ธฐ๋Šฅ : ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๋ฌด๋ฃŒ, State ํžˆ์Šคํ† ๋ฆฌ ๊ด€๋ฆฌ, State lock ๊ธฐ๋ณธ ์ œ๊ณต, State ๋ณ€๊ฒฝ์— ๋Œ€ํ•œ ๋น„๊ต ๊ธฐ๋Šฅ
  • Free Plan ์—…๋ฐ์ดํŠธ : ์‚ฌ์šฉ์ž 5๋ช… โ†’ ๋ฆฌ์†Œ์Šค 500๊ฐœ, ๋ณด์•ˆ ๊ธฐ๋Šฅ(SSO, Sentinel/OPA๋กœ Policy ์‚ฌ์šฉ) - ๋งํฌ

์›Œํฌ์ŠคํŽ˜์ด์Šค ์ƒ์„ฑ

  • https://app.terraform.io/ ๋งํฌ ์ ‘์† ํ›„ ๊ณ„์ • ์ƒ์„ฑ
  • workflow ์„ ํƒ ํ™”๋ฉด์—์„  Create a new organization ์„ ํƒ
  • Connect
  • GitHub์™€ ๊ฐ™์€ ๋ฒ„์ „๊ด€๋ฆฌ ์‹œ์Šคํ…œ์— ์—ฐ๊ฒฐํ• ๊ฑฐ๋ฉด VCS๋ฅผ ์„ ํƒํ•œ๋‹ค.
  • CLI-driven ์„ ํƒ

terraform login์„ ์ง„ํ–‰ํ•œ๋‹ค.

1
2
$ terraform login
[ํ† ํฐ ์ž…๋ ฅ]

ํ† ํฐ ํ™•์ธ

1
2
3
4
5
6
7
8
cat ~/.terraform.d/credentials.tfrc.json | jq
{
  "credentials": {
    "app.terraform.io": {
      "token": "YMgr4VM...EWGuw"
    }
  }
}

provider.tf์—์„œ ํ…Œ๋ผํผ ํด๋ผ์šฐ๋“œ๋ฅผ ์ •์˜ํ•œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
terraform {
  cloud {
    organization = "kane-org"         # ์ƒ์„ฑํ•œ ORG ์ด๋ฆ„ ์ง€์ •
    hostname     = "app.terraform.io" # default

    workspaces {
      name = "terraform-stduy" # ์—†์œผ๋ฉด ์ƒ์„ฑ๋จ
    }
  }
}

์ดํ›„, terraform init ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด .terraform ๋””๋ ‰ํ„ฐ๋ฆฌ๊ฐ€ ์ƒ์„ฑ๋˜๊ณ  ์•ˆ์— ์ƒํƒœํŒŒ์ผ์ด ์ƒ์„ฑ๋œ๋‹ค. ์•„๋ž˜๋Š” ์ƒํƒœํŒŒ์ผ ์„ธ๋ถ€๋‚ด์šฉ์ด๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
    "version": 3,
    "serial": 1,
    ...
    "backend": {
        "type": "cloud",
        "config": {
            "hostname": "app.terraform.io",
            "organization": "kane-org",
            "token": null,
            "workspaces": {
                "name": "terraform-stduy",
                "tags": null
            }
        },

์ด์ œ init && plan ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜๋ฉด, ๋‹ค์Œ๊ณผ ๊ฐ™์ด ํด๋ผ์šฐ๋“œ์—์„œ ๋™์ž‘ํ•œ๋‹ค. [terraform cloud local ์„ค์ •x]

์„ค์ •์„ ํ†ตํ•ด, terraform ์ž‘์—…์„ ๋ชจ๋‘ ๋กœ์ปฌ์—์„œ ๋Œ๋ฆฌ๊ณ  state ํŒŒ์ผ๋งŒ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ๋‹ค.

Plan์ด ๋ชจ๋‘ ๋™์ž‘ํ•˜๋ฉด, ์•„๋ž˜์™€ ๊ฐ™์ด UI๋กœ ๋ฐฐํฌ๋  ๋ฆฌ์†Œ์Šค๋ฅผ ์•Œ๋ ค์ค€๋‹ค.

ํ™•์‹คํžˆ GUI๋กœ ๋ณด๋‹ˆ ๊น”๋”ํ•œ ๊ฒƒ ๊ฐ™๋‹ค. ํŠนํžˆ ๋ฆฌ์†Œ์Šค๊ฐ€ ๋งŽ์•„์กŒ์„ ๋•Œ ๋ณด๊ธฐํŽธํ•  ๊ฒƒ ๊ฐ™๋‹ค.

๋„์ „๊ณผ์ œ3

๊ฐ์ž ์‚ฌ์šฉํ•˜๊ธฐ ํŽธ๋ฆฌํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ๋ชจ๋“ˆํ™” ํ•ด๋ณด๊ณ , ํ•ด๋‹น ๋ชจ๋“ˆ์„ ํ™œ์šฉํ•ด์„œ ๋ฐ˜๋ณต ๋ฆฌ์†Œ์Šค๋“ค ๋ฐฐํฌํ•ด๋ณด์„ธ์š”!

VPC, Subnet ๋“ฑ EC2์— ํ•„์š”ํ•œ ๋ฆฌ์†Œ์Šค๋ฅผ ๋ชจ๋“ˆํ™”ํ•ด๋ดค๋‹ค. ๋ณด์•ˆ๊ทธ๋ฃน์˜ ํฌํŠธ๋Š” SSH, HTTP๋ฅผ ์—ด์–ด๋†จ๋‹ค.

  • module/main.tf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    
      locals {
        additional_tags = {
          Name = var.namespace
        }
      }
        
      resource "aws_vpc" "vpc" {
        cidr_block = "192.169.0.0/16"
        tags       = local.additional_tags
      }
        
      data "aws_availability_zones" "available" {
        state = "available"
      }
        
      resource "aws_subnet" "public_subnet" {
        vpc_id                  = aws_vpc.vpc.id
        cidr_block              = "192.169.1.0/24"
        availability_zone       = data.aws_availability_zones.available.names[0]
        map_public_ip_on_launch = true
        tags                    = local.additional_tags
      }
        
      resource "aws_internet_gateway" "igw" {
        vpc_id = aws_vpc.vpc.id
        tags   = local.additional_tags
      }
      resource "aws_route_table" "public_route_table" {
        vpc_id = aws_vpc.vpc.id
        route {
          cidr_block = "0.0.0.0/0"
          gateway_id = aws_internet_gateway.igw.id
        }
        tags = local.additional_tags
      }
        
      resource "aws_route_table_association" "public_rtb_assoc" {
        
        subnet_id      = aws_subnet.public_subnet.id
        route_table_id = aws_route_table.public_route_table.id
      }
        
      resource "aws_security_group" "web_sg" {
        name   = var.namespace
        vpc_id = aws_vpc.vpc.id
        
        ingress {
          from_port   = var.ssh_port
          to_port     = var.ssh_port
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
        }
        
        ingress {
          from_port   = var.http_port
          to_port     = var.http_port
          protocol    = "tcp"
          cidr_blocks = ["0.0.0.0/0"]
        }
        
        egress {
          from_port   = 0
          to_port     = 0
          protocol    = "-1"
          cidr_blocks = ["0.0.0.0/0"]
        }
        
      }
      data "aws_ami" "default" {
        most_recent = true
        owners      = ["amazon"]
        
        filter {
          name   = "owner-alias"
          values = ["amazon"]
        }
        
        filter {
          name   = "name"
          values = ["amzn2-ami-hvm*"]
        }
      }
        
      resource "aws_instance" "app" {
        ami                    = data.aws_ami.default.id
        instance_type          = var.ec2_instance_type
        key_name               = var.key_name
        vpc_security_group_ids = [aws_security_group.web_sg.id]
        subnet_id              = aws_subnet.public_subnet.id
        tags                   = local.additional_tags
      }
    
  • module/output.tf

    1
    2
    3
    4
    
      output "instance_public_ip" {
        value       = aws_instance.app.public_ip
        description = "The public IP address of the App instance"
      }
    
  • module/variable.tf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
      variable "ssh_port" {
        default     = 22
        type        = number
        description = "SSH port"
      }
      variable "http_port" {
        default     = 80
        type        = number
        description = "HTTP port"
      }
      variable "ec2_instance_type" {
        type        = string
        description = "The type of EC2 instance to launch"
      }
      variable "key_name" {
        type        = string
        description = "The key name to use for an EC2 instance"
      }
      variable "namespace" {
        type        = string
        description = "env namespace"
      }
    
  • root/main.tf

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    
      locals {
        env = {
          dev = {
            instance_type = "t3.micro"
            key_name      = "m1"
            namespace     = "dev"
          }
          prod = {
            instance_type = "t3.medium"
            key_name      = "m1"
            namespace     = "prod"
          }
        }
      }
        
      provider "aws" {
        region = "ap-northeast-2"
      }
        
      module "ec2_aws_amazone" {
        for_each          = local.env
        source            = "../../module/ec2"
        key_name          = each.value.key_name
        ec2_instance_type = each.value.instance_type
        namespace         = each.value.namespace
      }
        
      # output.tf
      output "module_output_instance_public_ip" {
        value = [
          for k in module.ec2_aws_amazone : k.instance_public_ip
        ]
      }
    

์ด์ œ, ํ…Œ๋ผํผ ๋ช…๋ น์–ด๋ฅผ ์‹คํ–‰ํ•˜์—ฌ ๋ฆฌ์†Œ์Šค๋ฅผ ๋ฐฐํฌํ•œ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ tf apply -auto-approve
module.ec2_aws_amazone["dev"].data.aws_availability_zones.available: Reading...
module.ec2_aws_amazone["prod"].data.aws_availability_zones.available: Reading...
module.ec2_aws_amazone["prod"].data.aws_ami.default: Reading...
module.ec2_aws_amazone["dev"].data.aws_ami.default: Reading...
module.ec2_aws_amazone["dev"].data.aws_availability_zones.available: Read complete after 1s [id=ap-northeast-2]
module.ec2_aws_amazone["prod"].data.aws_availability_zones.available: Read complete after 1s [id=ap-northeast-2]
module.ec2_aws_amazone["dev"].data.aws_ami.default: Read complete after 1s [id=ami-0ec77cfb1037681eb]
module.ec2_aws_amazone["prod"].data.aws_ami.default: Read complete after 1s [id=ami-0ec77cfb1037681eb]
Apply complete! Resources: 14 added, 0 changed, 0 destroyed.

Outputs:

module_output_instance_public_ip = [
  "3.35.235.54",
  "13.124.205.208",
]

AWS ์ฝ˜์†”์—์„œ ๋ฐฐํฌ๋œ ๋ชฉ๋ก์„ ํ™•์ธํ•œ๋‹ค.

  • vpc

  • EC2

ํ™˜๊ฒฝ ๋ณ„๋กœ, ๋ฐฐํฌ๊ฐ€ ์ž˜ ๋œ ๋ชจ์Šต์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

This post is licensed under CC BY 4.0 by the author.