728x90
반응형
SMALL
SMALL

이전 시간에 Inventory 파일들을 만들어보고 알아보았다. 그럼 이 파일을 어떻게 수행하고 어떻게 실행해야 하는걸까?

여러 방법이 있지만 그 중 하나인 Adhoc 방법에 대해 알아보자.

 

Adhoc

쉽게 말하면, ansible이라는 명령어를 통해 수행하는 CLI 방식이라고 생각하면 된다. 형식은 다음과 같이 생겼다.

ansible [host-pattern] [-m module] [-a 'module options'] [-i inventory]
  • host-pattern: 그룹명을 의미한다. 
  • -m module: Adhoc 명령어를 통해 실행할 ansible 모듈을 의미한다.
  • -a module options: 모듈에 특정 옵션을 부여할 경우
  • -i inventory: 특정 인벤토리 파일의 경로

ping 모듈

그럼 다음 명령어 사용 방법을 기반으로 한번 아래를 수행해보자.

ansible -i amazon.inv -m ping all

참고로 위 사용 방법과 순서가 뒤죽박죽으로 되어 있다. 즉, 순서에 영향을 받지 않는다는 의미이다. 지금은 인벤토리 파일이 먼저 왔고 모듈이 그 다음에 왔고 호스트 패턴(그룹)이 제일 마지막에 왔음을 확인할 수 있다.

 

아무튼 이 명령어를 수행하면 다음과 같은 에러를 마주한다. 이전 포스팅에서 설명했지만, Ansible은 SSH로 접속할 수 있어야 하기 때문에 접속 정보가 필요한데, Amazon EC2의 유저 정보는 ec2-user이다. 그러나, 우리가 amazon.inv 파일에서는 별칭이나 ansible_user를 정의하지 않았기 때문에 현재 로컬 사용자로 접속하려고 시도한다. 당연히 없을것이다 해당 유저는. 그래서 문제가 발생한다.

 

그럼 amazon.inv 파일을 그대로 사용할 경우 유저 정보를 기입해줘야 하는데 다음과 같이 사용가능하다.

ansible -i amazon.inv -m ping all -u ec2-user

-u 옵션을 통해서 유저 정보를 줄 수 있다. 이것을 어떻게 알아냈냐면 그냥 -h 옵션으로 사용가능한 옵션을 확인해보면 된다. 이렇게 수행하면 다음과 같이 정상 결과를 돌려 받는다.

 

근데, 여기서 정상 결과를 돌려 받은 이유는 나는 ssh-agent라는 것을 사용해서 내 .pem 파일을 SSH 키 보관 장소에 보관했기 때문에 해당 EC2에 접근 가능한 Key pair 정보를 알려주지 않고도 접속이 가능한 것이다. ssh-agent를 사용하지 않는다면 --private-key라는 옵션을 주어서 .pem 파일 경로를 지정해줘야한다. ssh-agent를 사용한다면 .pem 파일을 등록해서 사용하면 굳이 SSH로 접속할 때마다 키 정보를 알려주지 않아도 되기 때문에 편리하다. 다음이 SSH-Agent가 키를 보관하는 저장소에 추가하는 방법이다.

ssh-add -K /path/to/.pem/

 

참고로 여기서 사용한 ping 모듈은 우리가 흔히 아는 ping이랑 다르다. 여기서 ping 모듈은 하는 작업이 2개다. 1. 대상 호스트에 연결 2. 파이썬 사용가능 여부 확인. 이렇게 두 가지를 해주는 모듈이다. 실제로 저 성공 결과 이미지를 보면 discovered_interpreter_python이라는 키에 파이썬 경로가 보여지는것을 알 수 있다. 이 작업을 하는 이유는 'Ansible'을 이용하게 되면 Control Node, Managed Node 이렇게 두 가지가 있다. 

Control Node는 지금 이 명령어를 수행하는 로컬 PC를 의미한다. 반면, Managed Node는 Ansible이 작업하는 서버인 AWS EC2를 의미한다. 이 예제에선 Amazon EC2를 말한다고 보면 된다. 당연히 Control Node에는 Ansible이 설치되어 있어야 하고 Managed Node는 파이썬이 설치가 되어 있어야 한다. 그래서 ping 모듈이 파이썬이 설치됐는지 확인한다.

 

command 모듈

이번엔 command라는 모듈을 사용해보자. 이는 Managed Node 내부에서 주어진 커맨드를 실행하는 모듈이다.

ansible -i vars.inv -m command -a "uptime" ubuntu

 

이번엔 vars.inv 파일을 사용해서 ansible_user도 작성이 된 파일을 사용하자. 따로 -u 옵션을 사용하기 귀찮으니까.

그리고 command 모듈을 사용해서 "uptime" 이라는 명령어를 ubuntu 그룹에만 수행하자. 

잘 수행됐음을 알 수 있다.

 

apt 모듈

이번엔 Ubuntu의 패키지 매니저인 apt-get을 사용할 수 있는 apt 모듈을 사용해서 패키지를 내려받아보자.

ansible -i vars.inv -m apt -a "name=git state=latest update_cache=yes" ubuntu

저렇게 하면 다음과 같은 에러가 발생할 것이다. 이는 무슨 에러냐면 호스트에 글로벌로 패키지를 설치하려면 기본적으로 root 권한이 있는 사용자가 설치해야 한다. 그러나 지금 vars.inv 파일에 기입된 유저는 ubuntu이므로 아래와 같은 권한 에러가 발생하는 것.

이를 해결하기 위해 다음과 같이 옵션을 붙일 수 있다.

ansible -i vars.inv -m apt -a "name=git state=latest update_cache=yes" ubuntu --become

--become 옵션은 사용자를 변경하는 옵션인데 기본적으로 root 사용자로 변경한다. 이렇게 실행해보자.

 

다음과 같이 잘 수행되었다. 이제 위에서 배운 command 모듈로 실제로 git이 잘 설치됐는지 확인해보자. 각각의 호스트에 이렇게 잘 설치됐음을 확인할 수 있다.

 

이 반대로 패키지를 설치할수도 있지만 삭제할수도 있다. 그럴땐 이렇게 작성하면 된다. state=absent는 해당 패키지를 제거한다는 의미이다. 이 명령어를 수행해보자.

ansible -i vars.inv -m apt -a "name=git state=absent update_cache=yes" ubuntu --become

 

잘 삭제됐는지 다시 한번 git 명령어를 사용해보자. 아래처럼 git을 찾을 수 없다고 나온다. 

 

 

결론

Ansible을 사용하는 방법 중 하나인 명령어로 수행하는 방식 'Adhoc' 방식을 알아보았다.

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Ansible Part. 4 (Handler)  (0) 2024.03.18
Ansible Part. 3 (Playbook)  (0) 2024.03.18
Ansible Part. 1 (Inventory)  (3) 2024.03.17
Packer Part. 5 (Post Processor)  (2) 2024.03.15
Packer Part. 4 (Data Source)  (0) 2024.03.14
728x90
반응형
SMALL
SMALL

 

이제 서버 형상 관리 도구인 "Ansible"을 사용해보자. Ansible을 사용하기에 있어 가장 먼저 이해해야 하는 개념은 Inventory이다.

 

Inventory

인벤토리란 Ansible을 사용하는 것은 특정 서버에 대해 형상 관리를 하기 위함인데 그렇다는 것은 특정 서버에 대한 정보가 필요하다. 그 정보를 관리하는 파일을 'Ansible'에서는 Inventory라고 한다.

 

이 Inventory는 그룹 기능을 지원하는데 예를 들어 내가 Linux 운영체제의 배포판 중 하나인 Ubuntu 서버를 3개를 관리하고 있다고 하면 Ubuntu라는 그룹을 만들어서 해당 그룹에 3개의 서버를 모두 할당할 수 있다. 그럼 그룹으로 실행해야 하는 명령어나 상태 체크같은 것들이 가능해지니 효율적인 관리가 될 수 있겠다.

 

이 인벤토리는 크게 두 가지 종류가 있다.

  • Static Inventory
  • Dynamic Inventory

Static inventory는 말 그대로 정적 인벤토리고 변경되지 않는 서버에 대한 관리를 할 때 사용한다고 보면 된다. 가장 기본이 되는 방식이고, Dynamic inventory는 AWS와 같은 클라우드 기반 서버의 IP는 수시로 변경될 수 있고 Auto Scaling과 같은 기능을 사용해서 없던 서버가 새로 생기는 경우도 비일비재하다. 이럴 때 동적으로 관리가 가능하게 해주는 방법이 Dynamic inventory라고 보면 되겠다.

 

Inventory 소스 작성과 이해

그러면 이제 실제로 AWS에 EC2 인스턴스 총 4개를 가지고 연습을 해보자.

우선 이전에 Terraform을 배웠을 때 Network, EC2를 생성하는 코드를 통해 총 4개의 EC2를 만들것이다.

 

이건 이전에 테라폼에 대한 포스팅에 올라와있으니 참고하면 되고 이미 생성됐다는 가정하에 진행하도록 하겠다.

4개 EC2를 테라폼을 통해 만들고 생성한 Outputs 결과는 다음과 같다.

 

이 정보들을 가지고 Inventory 파일들을 만들어보자. 우선 내 디렉토리 구조는 다음과 같이 생겼다. 참고로 인벤토리 파일의 확장자는 필요하지 않다. 그저 구분짓기 위해 작성했다고 보면 된다.

 

amazon.inv

이 파일은 amazon instance의 Public IP가 기록되어 있다.

3.38.149.218
52.78.112.88

 

ubuntu.inv

이 파일은 ubuntu instance의 Public DNS가 기록되어 있다. 이렇게 IP와 DNS를 사용하는 경우 둘 다 가능하다는 것을 보여주기 위해 작성했다.

ec2-3-34-189-200.ap-northeast-2.compute.amazonaws.com
ec2-54-180-160-242.ap-northeast-2.compute.amazonaws.com

 

simple.inv

이 파일은 위에서 말한 그룹 기능을 사용한 파일이다. 대괄호로 그룹을 지정하고 그 하위에 그룹의 대상이 되는 녀석들을 작성하면 된다.

참고로 Ansible inventory에서는 기본으로 가지고 있는 그룹이 있는데 'all' 이라는 그룹이다. 이 그룹은 inventory에 정의되어 있는 모든 그룹을 포함하고 있다고 보면 된다.

[amazon]
3.38.149.218
52.78.112.88
[ubuntu]
ec2-3-34-189-200.ap-northeast-2.compute.amazonaws.com
ec2-54-180-160-242.ap-northeast-2.compute.amazonaws.com

 

alias.inv

이 파일은 IP나 DNS값을 기억하기가 까다롭기 때문에 별칭을 사용해서 해당 값을 치환해 놓은 파일이다. 여기서 별칭은 (amazon1, amazon2, ubuntu1, ubuntu2)가 되고 이 별칭들은 당연히 Unique 값이어야 한다. 그리고 이 별칭에 대해서 ansible_host라는 변수에는 IP나 DNS값이 들어가면 된다. 그럼 이렇게 만들어 놓으면 추후에 3.38.149.218을 지칭하는 대신에 amazon1이라는 별칭으로 표현해주기 때문에 가시성에서 효율적일 수 있다.

[amazon]
amazon1 ansible_host=3.38.149.218
amazon2 ansible_host=52.78.112.88
[ubuntu]
ubuntu1 ansible_host=ec2-3-34-189-200.ap-northeast-2.compute.amazonaws.com
ubuntu2 ansible_host=ec2-54-180-160-242.ap-northeast-2.compute.amazonaws.com

 

vars.inv

이 파일은 변수를 추가하는 파일이다. 그래서 보면 없던 값인 ansible_user라는 값이 있다. 이는 무엇이냐면 Ansible은 Agentless하기 때문에 SSH나 WinRM을 통해 원격으로 명령을 수행하고 특정 작업을 진행한다. 그러려면 접속 유저 정보가 필요하다. Amazon EC2는 기본 사용자가 'ec2-user'이고 Ubuntu는 'ubuntu'이다. 그래서 이런 사용자를 지정하지 않고 만약 Ansible이 어떤 명령을 수행하기 위해 SSH접속을 할 때 로컬 PC의 현재 사용자와 동일한 사용자로 접속하고는 한다. 그러면 문제가 발생하기 때문에 이렇게 ansible_user라는 유저가 필요한 것이다.

[amazon]
amazon1 ansible_host=3.38.149.218 ansible_user=ec2-user 
amazon2 ansible_host=52.78.112.88 ansible_user=ec2-user

[ubuntu]
ubuntu1 ansible_host=ec2-3-34-189-200.ap-northeast-2.compute.amazonaws.com ansible_user=ubuntu
ubuntu2 ansible_host=ec2-54-180-160-242.ap-northeast-2.compute.amazonaws.com ansible_user=ubuntu

[linux:children]
amazon
ubuntu

 

그리고 하위에 작성한 [linux:children]이라는 표기가 있는데 이는 하위 그룹을 의미하는데 Linux라는 그룹이 amazon, ubuntu라는 그룹을 포함한다라는 의미라고 생각하면 된다.

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Ansible Part. 3 (Playbook)  (0) 2024.03.18
Ansible Part. 2 (Adhoc)  (3) 2024.03.17
Packer Part. 5 (Post Processor)  (2) 2024.03.15
Packer Part. 4 (Data Source)  (0) 2024.03.14
Packer Part. 3 (Provisioner)  (0) 2024.03.14
728x90
반응형
SMALL
SMALL

 

Post Processor

Post Processor란 말 그대로 후처리기이다. 패커가 빌드를 한 후에 실행되는 작업들을 정의하는 부분이라고 보면 되겠다.

문서를 보면 여러 Post Processor가 있는것을 확인할 수 있다.

 

Post-Processors | Packer | HashiCorp Developer

Post-processors run after the image is built by the builder and provisioned by the provisioner(s).

developer.hashicorp.com

이 중에서 자주 사용되는 몇가지를 알아보자.

Checksum

Packer로 빌드를 하면 보통은 산출물이 나오게 된다. 이를 Artifact라고 하는데, 이 Artifact를 가지고 후처리기가 또 다른 산출물을 만들게 된다. 그럼 Checksum은 어떤것이냐면, 해시함수를 이용해서 파일의 무결성을 검증하는 용도라고 생각하면 된다. 그래서 데이터 파일이 주어지면 해당 파일을 가지고 md5, sha256 같은 checksum type을 통해 해시값을 구하고 이 파일이 변조된 상태인지 아닌지를 판단할 수 있게 해준다.

 

Compress

빌드 결과물을 압축해주는 후처리기이다. 

 

Manifest

Packer가 빌드를 하면 빌드 결과에 대한 메타데이터를 가지는 파일이 있는데 이 파일을 만들어주는 후처리기이다.

 

Local Shell

사용자가 원하는 후처리기가 없을 때 커스텀하여 만드는 후처리기이다. 그래서 로컬 머신에서 원하는 명령어를 수행할 수 있게 된다.

 

 

Post Processor 사용해보기

이제 직접 후처리기를 사용해서 어떤식으로 동작하는지 확인해보자.

versions.pkr.hcl

packer {
    required_version = "~> 1.7"

    required_plugins {
        amazon = {
            version = "~> 1.0"
            source = "github.com/hashicorp/amazon"
        }
    }
}

sources.pkr.hcl

data "amazon-ami" "ubuntu" {
    filters = {
        virtualization-type = "hvm"
        name = "ubuntu/images/*ubuntu-focal-20.04-amd64-server-*"
        root-device-type = "ebs"
    }

    owners = ["099720109477"]
    most_recent = true
}

source "amazon-ebs" "ubuntu" {
    instance_type = "t2.micro"
    region = "ap-northeast-2"
    subnet_id = "subnet-0b23fd05b5919269e"
    associate_public_ip_address = true
    ssh_interface = "public_ip"
    source_ami = data.amazon-ami.ubuntu.id

    ssh_username = "ubuntu"
}

main.pkr.hcl

build {
    name = "cwchoiit-packer"

    source "amazon-ebs.ubuntu" {
        name = "nginx"
        ami_name = "cwchoiit-packer"
    }

    post-processor "manifest" {} # 첫번째 post-processor는 빌드 산출물을 다이렉트로 입력으로 받게 된다.

    post-processors {
        # post-processors의 첫번째 post-processor 역시 빌드 산출물을 다이렉트로 입력으로 받는다.
        post-processor "shell-local" {
            inline = ["echo Hello World ! > artifact.txt"]
        }
        # post-processors의 첫번째가 아닌 post-processor들은 첫번째 단계의 산출물을 입력으로 가져올 수 있게 된다.
        # 여기 같은 경우는 첫번째 post-processor가 shell-local이라 딱히 산출물이 없는데 파일(artifact.txt)을 만들어내는 스크립트가 있다.
        # 그리고 그 파일을 post-processor의 산출물로 만들고 싶으면 이 "artifice" 라는 후처리기를 사용하면 된다.
        # 그러면 이 files에 지정한 파일들을 다음 post-processor에게 전달하게 된다.
        post-processor "artifice" {
            files = ["artifact.txt"]
        }
        post-processor "compress" {}
    }

    post-processors {
        # post-processors의 첫번째 post-processor 역시 빌드 산출물을 다이렉트로 입력으로 받는다.
        post-processor "shell-local" {
            inline = ["echo Finished!"]
        }
    }
}

 

이 파일에 후처리기가 존재한다. 후처리기는 post-processor, post-processors 두 개의 블록으로 만들어 낼 수 있고 문자 그대로 복수개나 단일개냐의 차이가 있다. 그리고 후처리기를 사용하면서 알아야 할 것이 있는데 빌드가 끝난 후 최초의 post-processor와 post-processors의 첫번째 post-processor는 모두 빌드 산출물을 다이렉트로 입력으로 받게 된다. 그럼 post-processors의 두번째 세번째는 어떻게 동작하냐면 post-processors의 첫번째 post-processor의 산출물을 입력으로 가져올 수 있게 된다. 근데 저 예시에서 보면 첫번째 후처리기가 shell-local이고 이 녀석같은 경우 별다른 산출물을 만들어 내지 않는데 명령어로 artifact.txt라는 파일을 만들었다. 이 파일을 산출물로 만들고 싶으면 사용할 수 있는 다음 후처리기가 artifice라는 후처리기이다. 그 후처리기한테 files 리스트에 원하는 산출물을 넣으면 그 파일이 다음 후처리기에게 산출물로 적용된다.

 

이제, 이것을 실행해보자.

packer build .

 

아래 보면, Running post-processor 부분이 있다. 순서대로 manifest, shell-local, artifice, compress, shell-local 순으로 진행되는 모습을 볼 수 있다.

그리고 이렇게 빌드가 끝나면 실행한 경로에서 다음과 같이 새로운 파일들이 생겨났음을 볼 수 있다.

 

하나는 manifest 후처리기를 통해 만들어진 manifest 파일이고, 하나는 compress 후처리기를 통해 만들어진 압축파일이다. 압축파일을 풀어보자. 우선 어떤 형태의 파일인지 알아보기 위해 다음 명령어를 수행해보자.

보면 gzip 파일 형식으로 되어있다. 그래서 이름을 .gz 형태로 바꿔보자.

mv packer_ubuntu_amazon-ebs packer_ubuntu_amazon-ebs.gz

 

다음은, gzip형태의 파일을 압축 해제한다.

gunzip packer_ubuntu_amazon-ebs.gz

 

그 다음, 다시 파일의 형태를 살펴보면 tar 파일 형식임을 알 수 있다.

 

이제 tar 형식의 파일도 압축 해제하자.

tar xvzf packer_ubuntu_amazon-ebs

 

그럼 비로소 우리가 만든 artifact.txt 파일이 보여진다.

 

확인해보면 다음과 같은 결과를 얻는다.

 

 

결론

후처리기를 사용해서 패커 빌드 이후 작업을 수행해보았다. 

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Ansible Part. 2 (Adhoc)  (3) 2024.03.17
Ansible Part. 1 (Inventory)  (3) 2024.03.17
Packer Part. 4 (Data Source)  (0) 2024.03.14
Packer Part. 3 (Provisioner)  (0) 2024.03.14
Packer Part. 2 (Builder)  (0) 2024.03.14
728x90
반응형
SMALL
SMALL

 

Data Source

Terraform에서 알던 Data Source와 정확히 일치한다. 그래서 바로 예제 코드를 실행해보면서 어떻게 동작하는지 보자.

 

versions.pkr.hcl

packer {
    required_version = "~> 1.7"

    required_plugins {
        amazon = {
            version = "~> 1.0"
            source = "github.com/hashicorp/amazon"
        }
    }
}

 

sources.pkr.hcl

여기서 data 블록이 Data Source이다. Ubuntu AMI를 Data Source로 가져오는 방식을 취했다. 이 방법의 이점은 source가 많아지는데 같은 Ubuntu AMI를 사용할 때 중복 코드를 제거해줄 수 있겠다.

data "amazon-ami" "ubuntu" {
    filters = {
        virtualization-type = "hvm"
        name = "ubuntu/images/*ubuntu-focal-20.04-amd64-server-*"
        root-device-type = "ebs"
    }

    owners = ["099720109477"]
    most_recent = true
}

source "amazon-ebs" "ubuntu" {
    instance_type = "t2.micro"
    region = "ap-northeast-2"
    subnet_id = "subnet-0b23fd05b5919269e"
    associate_public_ip_address = true
    ssh_interface = "public_ip"
    source_ami = data.amazon-ami.ubuntu.id

    ssh_username = "ubuntu"
}

 

main.pkr.hcl

여기서도 Data Source 하나가 더 사용된다. amazon-secretsmanager 라는 Data Source이다. 

data "amazon-secretsmanager" "cwchoiit" {
    name = "cwchoiit"
    key = "test"
}

build {
    name = "cwchoiit-packer"

    source "amazon-ebs.ubuntu" {
        name = "nginx"
        ami_name = "cwchoiit-packer-nginx"
    }

    provisioner "shell" {
        inline = [
            "sudo apt-get update",
            "echo Secret is ${data.amazon-secretsmanager.cwchoiit.value}"
        ]
    }

    provisioner "file" {
        source = "${path.root}/files/index.html"
        destination = "/tmp/index.html"
    }

    provisioner "shell" {
        inline = [
            "echo ${source.name} and ${source.type}",
            "whoami",
            "sudo apt-get install -y nginx",
            "sudo cp /tmp/index.html /var/www/html/index.html"
        ]
    }
}

 

AWS에서 제공하는 Secrets Manager 서비스에 대한 Data Source이다. Sensitive한 데이터를 저장하고 보관할 때 이 서비스를 활용할 수 있겠다. name 속성은 Secret Manager의 Secret name을 나타낸다. key는 해당 데이터에 들어가 있는 Key/Value의 Key를 나타낸다.

저 값을 가져오는 Data Source라고 생각하면 된다. 그리고 그 아래는 전에 봤던 build 블록이다. 그리고 provisioner에서 보면 Secret의 value값을 찍는 명령어가 보인다. 한번 build 해보자.

packer build .

 

이런식으로 Terraform과 정확히 동일하게 같은 개념으로 Data Source를 이해할 수 있었다.

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Ansible Part. 1 (Inventory)  (3) 2024.03.17
Packer Part. 5 (Post Processor)  (2) 2024.03.15
Packer Part. 3 (Provisioner)  (0) 2024.03.14
Packer Part. 2 (Builder)  (0) 2024.03.14
Packer Part. 1  (0) 2024.03.14
728x90
반응형
SMALL
SMALL

 

Provisioner

Provisioner는 머신 이미지 내부에 필요한 설정이나 소프트웨어를 설치할 때 사용한다. 예를 들면 필요 패키지 설치, 사용자 생성 등 이런 머신 이미지 내 필요한 작업을 Provisioner를 통해서 할 수 있다. 그리고 이 Provisioner의 종류가 굉장히 많다. 문서를 참조하면 더 많은 정보를 알 수 있다. 

 

그럼 직접 사용해보자.

 

versions.pkr.hcl

각 버전 정보들을 기입한 파일이다. 특이 사항은 없다.

packer {
    required_version = "~> 1.7"

    required_plugins {
        amazon = {
            version = "~> 1.0"
            source = "github.com/hashicorp/amazon"
        }
    }
}

 

sources.pkr.hcl

빌드에 필요한 source 정보를 기입한 파일이다. Amazon EBS 리소스를 가진 AMI를 만들 예정이다. 지난 시간에 작성한 내용과 일치하니 특이 사항은 없다.

source "amazon-ebs" "ubuntu" {
    instance_type = "t2.micro"
    region = "ap-northeast-2"
    subnet_id = "subnet-0b23fd05b5919269e"
    associate_public_ip_address = true
    ssh_interface = "public_ip"

    source_ami_filter {
        filters = {
            name = "ubuntu/images/*ubuntu-focal-20.04-amd64-server-*"
            root-device-type = "ebs"
            virtualization-type = "hvm"
        }

        most_recent = true
        owners = ["099720109477"]
    }

    ssh_username = "ubuntu"
}

 

files/index.html

<h1>Hello Packer</h1>

 

main.pkr.hcl

이 파일에서 실제로 어떤것을 빌드할지를, 빌드 후 어떤 처리를 할지를 정의한 파일이다. 

build {
    name = "cwchoiit-packer"

    source "amazon-ebs.ubuntu" {
        name = "nginx"
        ami_name = "cwchoiit-packer-nginx"
    }

    # Provisioner는 정의한 순서대로 실행하기 때문에 순서가 중요하다.
    provisioner "shell" {
        inline = [
            "sudo apt-get update",
            "whoami",
        ]
    }

    provisioner "file" {
        source = "${path.root}/files/index.html"
        destination = "/tmp/index.html"
    }

    provisioner "shell" {
        inline = [
            "echo ${source.name} and ${source.type}",
            "whoami",
            "sudo apt-get install -y nginx",
            "sudo cp /tmp/index.html /var/www/html/index.html"
        ]
    }

    provisioner "breakpoint" {
        disable = false
        note = "For debugging"
    } 
}

 

source는 위에서 작성한 amazon-ebs.ubuntu를 사용한다. 그리고 name, ami_name을 expand했다.

 

이제 provisioner가 4개 있다. shell provisioner는 내부에서 명령어를 통해 어떤 작업을 하기 위해서 사용한다.

 

첫번째 shell provisioner는 ubuntu의 패키지 매니저인 apt-get update를 하고 현재 사용자가 누구인지를 알려주는 명령어를 실행한다.

두번째 file provisioner는 로컬의 파일을 내부로 복사하는 역할을 한다. path.root는 이 build 블록이 있는 파일을 가리킨다.

세번째 shell provisioner는 source라는 녀석을 가져와 명령어에 사용할 수 있음을 보여주기 위해 작성하고, 현재 유저가 누구인지와, nginx를 설치하는 작업, 복사한 파일을 nginx가 띄우는 경로로 이동하는 작업까지 진행한다.

네번째 breakpoint provisioner는 디버깅용으로 사용된다. 작업 중에 잠시 멈춰진다. 다음과 화면을 보자.

이 상태에서 사용자가 Enter를 입력해야 다음으로 넘어간다.

 

이제, Enter를 입력하기 전에 Build를 한 내용대로 인스턴스가 만들어졌는지 확인해보자. (Enter를 입력하면 AMI를 만들어내기 위한 모든 작업을 끝마쳤기 때문에 인스턴스를 중지하고 종료해서 인스턴스에서 작업된 내용을 확인할 수 없다.)

Build 과정은 머신위에 Nginx를 설치하고 파일 하나를 옮겼고 그 파일을 Nginx가 웹 상에 호스팅하게 했다. 한번 확인해보자. 우선 생성된 인스턴스의 보안 그룹에 80 포트를 허용해주자.

 

그래야 80 포트로 밖에서 접속할 수 있다. 그 다음, 해당 인스턴스의 Public IP로 들어가보면 위 index.html 파일이 보여진다.

이렇게 우리가 설정한 대로 Provisioner가 작동한 것이다. 이런 설정 및 패키지 설치와 같은 작업을 Provisioner가 한다고 보면 된다.

그리고 반드시 명심할 것: Provisioner는 작성한 순서대로 동작한다. 이제 엔터를 입력해서 끝마치자.

 

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Packer Part. 5 (Post Processor)  (2) 2024.03.15
Packer Part. 4 (Data Source)  (0) 2024.03.14
Packer Part. 2 (Builder)  (0) 2024.03.14
Packer Part. 1  (0) 2024.03.14
Terraform Provisioner/EC2 Userdata  (2) 2024.03.11
728x90
반응형
SMALL
SMALL

 

Builder

Packer에서 Builder는 어떤 이미지를 만들것이냐?에 대한 물음이다. 예를 들어 Amazon EBS에 대한 이미지를 만들고 싶다면 Builder는 Amazon EBS가 된다. 그래서 Builder에 대해 간략하게 알아보자.

 

Null Builder

null 빌더는 말 그대로 아무것도 만들어내지 않겠다는 의미이다. 이걸 그럼 왜쓰냐? 일반적으로 디버깅이 필요할 때 사용한다고 한다. (공식문서 참조) 우리도 이 Null builder로 각종 테스트를 해보자.

 

 

versions.pkr.hcl

packer {
    required_version = "~> 1.7"

    required_plugins {
        amazon = {
            version = "~> 1.0"
            source = "github.com/hashicorp/amazon"
        }
    }
}

 

sources.pkr.hcl

source "null" "one" {
    communicator = "none"
}

source "null" "two" {
    communicator = "none"
}

저번 시간에도 source가 바로 어떻게 빌드할 것인지에 대한 내용이라고 했는데, 이처럼 null은 뭐가 거의 없다. communicator = "none"이 의미하는 건 이 빌더랑 Packer가 통신하는 방법(SSH, WinRM)은 필요 없다는 의미이다.

 

main.pkr.hcl

# Without name
build {
    sources = [
        "source.null.one",
        "source.null.two",
    ]
}

# With name
build {
    name = "cwchoiit-packer"

    sources = [
        "source.null.one",
        "source.null.two",
    ]
}

# Fill-in (기존 source를 확장하는 기능, 근데 원래 있던 것을 변경하는 것은 불가능하다. 예를 들어, 기존에 null.one이 가지고 있던 communicator를 여기서 변경하는 행위)
build {
    name = "cwchoiit-packer-fill-in"
    
    source "null.one" {
        name = "terraform"
    }

    source "null.two" {
        name = "vault"
    }
}

이 부분을 보면 세 개의 build 블록으로 나뉘어져 있다. 빌드의 이름이 없는 경우, 빌드의 이름이 있는 경우, 빌드의 이름이 있고 source를 expand하는 경우 총 세가지다. 이런 각각의 방법으로 build를 할 수가 있다는 것을 보여준다.

 

빌드를 해보자.

packer build .

 

이렇게 각 빌드별로 달라지는 Outputs을 볼 수 있다. 

 

Build only/except

원하는 것만 빌드하고 싶을 때 사용하는 옵션을 알아보자.

packer build -only=null.one .

이처럼 원하는 소스만 빌드할 수 있다.

 

특정 소스만 제외하고 빌드를 하고 싶다면 except 옵션을 사용하면 된다.

packer build -except=null.one .

제외 시킨 것 말고 다 빌드가 되는 모습이다.

 

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Packer Part. 4 (Data Source)  (0) 2024.03.14
Packer Part. 3 (Provisioner)  (0) 2024.03.14
Packer Part. 1  (0) 2024.03.14
Terraform Provisioner/EC2 Userdata  (2) 2024.03.11
terraform_remote_state  (0) 2024.03.10
728x90
반응형
SMALL
SMALL

Packer

hashicorp에서 제공하는 Packer라는 도구를 사용해보자. 우선 Packer는 멀티 플랫폼 이미지 빌드 도구이다. 예를 들면 AMI, Docker 등 여러 가상 머신위에 올리기 위해 사용되는 머신 이미지를 HCL 문법으로 만들어 낼 수 있다. 

 

간단하게 EBS가 내재된 AMI를 만들어보는 예제를 통해 시작해보자.

 

main.pkr.hcl

packer {
    required_version = "~> 1.7"    

    required_plugins {
        amazon = {
            version = "~> 1.0"
            source = "github.com/hashicorp/amazon"
        }
    }
}

source "amazon-ebs" "ubuntu" {
    ami_name = "cwchoiit-packer"
    instance_type = "t2.micro"
    region = "ap-northeast-2"
    subnet_id = "subnet-0b23fd05b5919269e"
    associate_public_ip_address = true
    ssh_interface = "public_ip"

    source_ami_filter {
        filters = {
            name = "ubuntu/images/*ubuntu-focal-20.04-amd64-server-*"
            root-device-type = "ebs"
            virtualization-type = "hvm"
        }
        most_recent = true

        owners = ["099720109477"]
    }

    ssh_username = "ubuntu"
}

build {
    name = "cwchoiit-packer"
    sources = [
        "source.amazon-ebs.ubuntu"
    ]
}

 

우선 첫 부분인 다음 코드를 보자. Packer를 사용하기 위한 버전 명시와 어떤 플랫폼의 이미지를 만들어 낼 것인가에 대한 Plugin을 작성하는 부분이다. 

packer {
    required_version = "~> 1.7"    

    required_plugins {
        amazon = {
            version = "~> 1.0"
            source = "github.com/hashicorp/amazon"
        }
    }
}

 

이제 source 부분이다. 이 부분이 이미지를 만들어 내기 위해 필요한 내용이 담긴 부분이다.

source "amazon-ebs" "ubuntu" {
    ami_name = "cwchoiit-packer"
    instance_type = "t2.micro"
    region = "ap-northeast-2"
    subnet_id = "subnet-0b23fd05b5919269e"
    associate_public_ip_address = true
    ssh_interface = "public_ip"

    source_ami_filter {
        filters = {
            name = "ubuntu/images/*ubuntu-focal-20.04-amd64-server-*"
            root-device-type = "ebs"
            virtualization-type = "hvm"
        }
        most_recent = true

        owners = ["099720109477"]
    }

    ssh_username = "ubuntu"
}

 

Terraform을 공부한 상태다보니 이해가 안가는 부분은 없다. 여기서 subnet_id를 명시한 이유는 나 같은 경우 Default VPC가 없기 때문에 반드시 Subnet을 명시해줘야 한다. 그리고 associate_public_ip_address도 true로 설정하지 않으면 AMI를 만들기 위해 새로 생성되는 EC2의 Public IP가 존재하지 않기 때문에 SSH 접속을 Packer가 할 수 없다. 그래서 'true'로 설정해줘야 한다. 

 

source_ami_filter는 기반이 되는 AMI를 지정하는 부분이다. ubuntu 이미지 가장 최신 버전을 사용하기로 한다. 

 

마지막 부분인 build 부분이다. 이 부분은 빌드의 이름과 어떤 것을 빌드할 것인지에 대한 정보를 기입한다. 

build {
    name = "cwchoiit-packer"
    sources = [
        "source.amazon-ebs.ubuntu"
    ]
}

 

이렇게 작성한 파일을 가지고 다음 명령어를 입력한다. 

packer init .

우선 init 명령어를 통해 필요한 plugin을 내려받아야 한다. 이 명령어가 잘 수행되면 다음 명령어를 입력한다.

packer build .

 

이제 빌드를 시작한다. 빌드를 시작하면 다음과 같은 화면이 보일것이다. 

과정을 보면 알겠지만 일시적으로 SSH에 접속하기 위해 Key pair와 Security group을 생성하는것이 보인다. 이 부분도 명시해서 가져다가 사용하게 할 수 있지만 이 예제에선 Packer가 직접 만들도록 했다. 그리고 인스턴스가 생성된 후 SSH로 인스턴스에 접속한다. 그 이유는 AMI는 인스턴스에서 필요한 모든 패키지나 기본 설정을 다 한 상태에서의 이미지이기 때문에 그런것들이 있다면 해당 인스턴스에서 설치 작업을 수행해야 하기 때문에 SSH에 접속하는 것이다. 그러나 이 예제에는 따로 설치할 것은 없다. 내가 정의하지 않았으니까. 이렇게 인스턴스가 만들어지고 AMI가 만들어지면 일시적으로 만든 Security group, Key pair, Instance가 삭제된다. 

 

그리고 그 결과로 이런 AMI가 만들어진다. AWS Console에서도 확인 가능하다.

 

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Packer Part. 3 (Provisioner)  (0) 2024.03.14
Packer Part. 2 (Builder)  (0) 2024.03.14
Terraform Provisioner/EC2 Userdata  (2) 2024.03.11
terraform_remote_state  (0) 2024.03.10
Terraform 나만의 Module 만들어보기  (0) 2024.03.10
728x90
반응형
SMALL
SMALL

테라폼에서 Provisioner가 어떤것이고 EC2 Userdata는 무엇이며 둘 간의 차이는 무엇인지 알아보자.

 

 

Provisioner

테라폼에서 공식적으로 지원하는 문법이다. 세 가지 종류가 있는데 다음과 같다.

  • file: 로컬 -> 리모트 파일복사
  • local-exec: 로컬PC에서 명령어 수행
  • remote-exec: 리모트 머신에서 명령어 수행 (SSH, WinRM 과 같은 프로토콜을 지원)

그래서 위 세가지 경우를 처리하기 위해 테라폼에서 provisioner라는 녀석을 제공해준다고 생각하면 된다.

이 녀석은 기본적으로는 첫 리소스 생성 시점에 수행되지만 여러 옵션들을 통해 삭제 시점이라던가 매번 수행할 수 있도록 커스터마이징 할 수도 있다.

EC2 Userdata

우선 Userdata는 무엇이냐? AWS EC2 인스턴스를 만들 때 하단에 Advanced Settings 설정을 열어보면 Userdata가 보일것이다.

이 작업은 부팅 시점에 부트스트래핑을 해주는 녀석이다. 예를 들면, 사용자를 생성한다거나, 파일을 구성한다거나, 소프트웨어 설치를 한다거나 하는 작업들을 첫 부팅 시점에만 실행을 해준다.

 

 

실습

그럼 이제 provisioner를 이용해서 EC2를 만들어보는것과 Userdata를 이용해서 EC2를 만들어보고 어떤 차이점이 있고 어떻게 돌아가는지 직접 확인해보자.

 

다음이 이 내용을 실습하기 위한 파일 구조이다.

하나씩 살펴보자.

 

versions.tf

이 파일은 기본적으로 요구되는 버전을 명시한 파일이다. 특이사항은 없다.

terraform {
  required_version = "~> 1.0"

  required_providers {
    aws = {
        source = "hashicorp/aws"
        version = "~> 3.0"
    }
  }
}

 

outputs.tf

provisioner와 userdata를 각각 이용해서 만든 EC2 인스턴스에 대한 Public/Private IP와 DNS관련 데이터이다. 특이사항은 없다.

output "provisioner_instance" {
  value = {
    public_ip = aws_instance.provisioner.public_ip
    public_dns = aws_instance.provisioner.public_dns
    private_ip = aws_instance.provisioner.private_ip
    private_dns = aws_instance.provisioner.private_dns
  }
}

output "userdata_instance" {
  value = {
    public_ip = aws_instance.userdata.public_ip
    public_dns = aws_instance.userdata.public_dns
    private_ip = aws_instance.userdata.private_ip
    private_dns = aws_instance.userdata.private_dns
  }
}

files/index.html

이 파일은 nginx로 띄워줄 index.html 파일이다. 특이사항은 없다.

<h1>Hello Chyonee!</h1>

files/install-nginx.sh

이 파일은 nginx 설치 관련 ShellScript 파일이다. 역시 특이사항은 없다.

#!/bin/bash

sudo apt-get update
sudo apt-get install -y nginx

 

main.tf

provider "aws" {
  region = "ap-northeast-2"
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"]
}

locals {
  vpc_name = "cwchoiit-vpc"
  common_tags = {
    "Project" = "provisioner-userdata"
  }
}

module "security_group" {
  source = "tedilabs/network/aws//modules/security-group"

  version = "0.24.0"

  name        = "${local.vpc_name}-provisioner-userdata"
  description = "Security Group for SSH."
  vpc_id      = "vpc-04a888e12b489eb12"

  ingress_rules = [
    {
      id          = "ssh"
      protocol    = "tcp"
      from_port   = 22
      to_port     = 22
      cidr_blocks = ["0.0.0.0/0"]
      description = "Allow SSH from anywhere."
    },
    {
      id          = "http"
      protocol    = "tcp"
      from_port   = 80
      to_port     = 80
      cidr_blocks = ["0.0.0.0/0"]
      description = "Allow HTTP from anywhere."
    }
  ]

  egress_rules = [
    {
      id          = "all/all"
      description = "Allow to communicate to the Internet."
      protocol    = "-1"
      from_port   = 0
      to_port     = 0
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]

  tags = local.common_tags
}

resource "aws_instance" "userdata" {
  ami                         = data.aws_ami.ubuntu.image_id
  instance_type               = "t2.micro"
  key_name                    = "cwchoiit-ec2-keypair-home"
  subnet_id                   = "subnet-0b23fd05b5919269e"
  associate_public_ip_address = true

  user_data = <<EOT
#!/bin/bash
sudo apt-get update
sudo apt-get install -y nginx
EOT

  vpc_security_group_ids = [
    module.security_group.id,
  ]

  tags = {
    Name = "cwchoiit-userdata"
  }
}

resource "aws_instance" "provisioner" {
  ami                         = data.aws_ami.ubuntu.image_id
  instance_type               = "t2.micro"
  key_name                    = "cwchoiit-ec2-keypair-home"
  subnet_id                   = "subnet-0b23fd05b5919269e"
  associate_public_ip_address = true

  vpc_security_group_ids = [
    module.security_group.id,
  ]

  tags = {
    Name = "cwchoiit-provisioner"
  }
}

resource "null_resource" "provisioner" {
  triggers = {
    instance_id = aws_instance.provisioner.id
    script      = filemd5("${path.module}/files/install-nginx.sh")
    index_file  = filemd5("${path.module}/files/index.html")       
  }

  provisioner "local-exec" {
    command = "echo Hello World"
  }

  provisioner "file" {
    source      = "${path.module}/files/index.html"
    destination = "/tmp/index.html"

    connection {
      type        = "ssh"
      user        = "ubuntu"
      host        = aws_instance.provisioner.public_ip
      private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
    }
  }

  provisioner "remote-exec" {
    script = "${path.module}/files/install-nginx.sh"

    connection {
      type        = "ssh"
      user        = "ubuntu"
      host        = aws_instance.provisioner.public_ip
      private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
    }
  }

  provisioner "remote-exec" {
    inline = [
      "sudo cp /tmp/index.html /var/www/html/index.html"
    ]

    connection {
      type        = "ssh"
      user        = "ubuntu"
      host        = aws_instance.provisioner.public_ip
      private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
    }
  }
}

 

하나씩 뜯어보자. 위 부분(AWS Provider, AMI 가져오는 Data Source, Local Variables은 생략하겠다. 너무 많이 봐왔던 것들이기 때문)

 

EC2에 할당할 Security Group은 누군가가 만들어놓은 Module을 가져다가 사용했다. Inbound/Outbound 규칙은 ingress, egress에 매핑된 상태고, name, description, vpc_id를 할당해주었다.

module "security_group" {
  source = "tedilabs/network/aws//modules/security-group"

  version = "0.24.0"

  name        = "${local.vpc_name}-provisioner-userdata"
  description = "Security Group for SSH."
  vpc_id      = "vpc-04a888e12b489eb12"

  ingress_rules = [
    {
      id          = "ssh"
      protocol    = "tcp"
      from_port   = 22
      to_port     = 22
      cidr_blocks = ["0.0.0.0/0"]
      description = "Allow SSH from anywhere."
    },
    {
      id          = "http"
      protocol    = "tcp"
      from_port   = 80
      to_port     = 80
      cidr_blocks = ["0.0.0.0/0"]
      description = "Allow HTTP from anywhere."
    }
  ]

  egress_rules = [
    {
      id          = "all/all"
      description = "Allow to communicate to the Internet."
      protocol    = "-1"
      from_port   = 0
      to_port     = 0
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]

  tags = local.common_tags
}

 

이제 Userdata를 이용해서 AWS EC2 인스턴스를 만드는 부분이다. 기본적으로 ami, instance_type, subnet_id, associate_public_ip_address는 할당해주었다. 여기서 새롭게 보이는 key_name은 EC2에 SSH로 접속할 때 사용했던 Key pair의 Name이다. 그리고 이제 핵심 부분인 user_data가 있다. 보면 EOT로 시작해서 EOT로 끝나는데 그냥 Multiple 문자열을 받기 위해 저렇게 사용한다. 그리고 첫 부팅시에만 적용될 sudo apt-get updatenginx를 설치하는 작업을 한다. 그 다음 위에서 설명한 security_group을 지정했다. 

resource "aws_instance" "userdata" {
  ami                         = data.aws_ami.ubuntu.image_id
  instance_type               = "t2.micro"
  key_name                    = "cwchoiit-ec2-keypair-home"
  subnet_id                   = "subnet-0b23fd05b5919269e"
  associate_public_ip_address = true

  user_data = <<EOT
#!/bin/bash
sudo apt-get update
sudo apt-get install -y nginx
EOT

  vpc_security_group_ids = [
    module.security_group.id,
  ]

  tags = {
    Name = "cwchoiit-userdata"
  }
}

 

그 다음 EC2 하나를 더 만든다. 이 부분은 provisioner를 사용하는 부분은 없다. 그 바로 밑에 있는데 우선 리소스 이름 자체는 provisioner로 설정했다. 위 내용과 설명은 동일하다.

resource "aws_instance" "provisioner" {
  ami                         = data.aws_ami.ubuntu.image_id
  instance_type               = "t2.micro"
  key_name                    = "cwchoiit-ec2-keypair-home"
  subnet_id                   = "subnet-0b23fd05b5919269e"
  associate_public_ip_address = true

  vpc_security_group_ids = [
    module.security_group.id,
  ]

  tags = {
    Name = "cwchoiit-provisioner"
  }
}

 

 

이제 이 부분이 핵심이다. null_resource는 이 역시 Terraform에서 지원하는 Provider 중 하나인데 이 녀석은 딱 한가지 Attribute를 가지고 있다. triggers 라는 Attribute인데 이 triggers는 그 안에 지정한 것들 중 하나라도 변경 사항이 생기면 이 null_resource라는 전체 리소스를 재실행한다. 즉 리소스를 다시 만들어낸다. 그리고 이것을 사용해서 실제 provisioner를 사용했다.

 

resource "null_resource" "provisioner" {
  triggers = {
    instance_id = aws_instance.provisioner.id
    script      = filemd5("${path.module}/files/install-nginx.sh") 
    index_file  = filemd5("${path.module}/files/index.html")       
  }

  provisioner "local-exec" {
    command = "echo Hello World"
  }

  provisioner "file" {
    source      = "${path.module}/files/index.html"
    destination = "/tmp/index.html"

    connection {
      type        = "ssh"
      user        = "ubuntu"
      host        = aws_instance.provisioner.public_ip
      private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
    }
  }

  provisioner "remote-exec" {
    script = "${path.module}/files/install-nginx.sh"

    connection {
      type        = "ssh"
      user        = "ubuntu"
      host        = aws_instance.provisioner.public_ip
      private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
    }
  }

  provisioner "remote-exec" {
    inline = [
      "sudo cp /tmp/index.html /var/www/html/index.html"
    ]

    connection {
      type        = "ssh"
      user        = "ubuntu"
      host        = aws_instance.provisioner.public_ip
      private_key = file("${path.module}/../cwchoiit-ec2-keypair-home.pem")
    }
  }
}

 

triggers에 3개가 있다. instance_id, script, index_file. 우선 instance_id는 위에서 만든 provisioner라는 이름의 리소스를 가진 리소스의 ID가 변경되거나, script, index_file은 files 폴더에 있는 파일의 해시값이 변경된다(파일 내용이 변경됨)면 이 리소스가 재실행된다는 의미이다.

 

여기에 local-exec는 로컬 PC에서 명령어를 수행하는 부분이다. 의미없지만 잘 수행되는지 Hello World를 echo로 실행한다.

file은 로컬에서 리모트로 파일을 전송한다. 그래서 sourcedestination 속성이 있다. 저 부분은 뭐가 뭔지 한 눈에 알 수 있을것이고 connection 부분이 중요하다. 이 부분이 어떻게 전송할지에 대한 내용이기 때문이다. 우선 SSH로 보내고 유저 이름은 "ubuntu"이다. Host는 위에서 provisioner라는 리소스 이름의 인스턴스 Public IP가 된다. 그리고 private_key가 이제 EC2에 SSH로 접속할 때 사용하는 .pem 파일이다. 이 .pem 파일은 AWS Console에서 EC2 인스턴스 만들 때 Key pair로 사용하는 그 .pem 파일이다.

 

그리고 remote-exec는 리모트 머신에서 명령어를 수행한다. 그 수행하는 명령어가 바로 script = "실행할 스크립트 파일의 경로" 또는 inline = ["명령어", "명령어"]이다. 이 명령어를 작성하는 방법은 세가지가 있다. script, scripts, inline. script와 inline은 보았고, scripts는 스크립트 파일 한개가 아니라 여러개를 실행할 때 사용한다.

 

그래서 이 main.tf에 대해 Apply를 하면 기존에 있는 VPC를 사용해서(저 위에 vpc-id가 기존에 있는 내 VPC ID를 붙인 것) 해당 VPC의 Subnet역시 기존에 있는 Subnet에 EC2 두개가 만들어진다. (Userdata로, Provisioner로) 그리고 각 EC2는 Nginx를 설치하는데 Userdata는 설치하는 것까지가 끝이다. 그리고 그것은 딱 한번 최초 부팅시에만 실행될 것이다. 그게 Userdata의 특징이니까. 그러나 provisioner로 만든 EC2는 null_resource를 사용해서 감시하는 3개의 트리거가 발동될 때마다 새롭게 null_resource에 작성한 작업들을 수행하게 된다. 왜냐하면 null_resource에서 작업하기로 하는 HOST가 전부 provisioner로 만든 EC2의 Public IP를 가르키기 때문.

 

그래서 이 것을 Apply해보자. 다음과 같이 정상적으로 Apply가 됐고 EC2 인스턴스 두개가 생겼다.

 

AWS Console에 들어가보면 만든 EC2 두개가 보인다.

 

그리고 각 EC2의 Security도 잘 적용된 상태다.

 

이 상태에서 저 Outputs에서 알려준 Public IP를 접근해보자. Nginx를 두 EC2 모두 설치했기 때문에 80으로 접속 시 Nginx 화면이 보여야한다.

 

Userdata로 만든 EC2

 

Provisioner로 만든 EC2

 

Provisioner로 만든 EC2는 왜 다를까? null_resource에서 index.html 파일을 Nginx가 기본으로 뿌려주는 경로에 위치시켰기 때문에 내가 만든 이 index.html 파일이 보이는 것이다.

 

그럼 여기서 한 발 더 나아가서 이 파일(index.html)이 수정되면? => 트리거가 발동 => provisioner로 만든 EC2가 Replace 될 것

files/index.html

다음과 같이 수정해보자.

<h1>Hello Chyoni!</h1>

 

그 후 Apply 진행. 변경된다는 Plan이 보인다.

 

작업이 진행되고 다시 provisioner로 띄운 EC2로 들어가보면 다음과 같이 변경 사항이 적용됐음을 확인 가능하다.

 

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Packer Part. 2 (Builder)  (0) 2024.03.14
Packer Part. 1  (0) 2024.03.14
terraform_remote_state  (0) 2024.03.10
Terraform 나만의 Module 만들어보기  (0) 2024.03.10
Terraform Cloud  (0) 2024.03.10
728x90
반응형
SMALL
SMALL

 

terraform_remote_state는 어떤것이고 어떻게 활용하는지 알아보자.

terraform_remote_state

이 terraform_remote_state는 Data Source로써 사용이 가능하다. hasicorp에서 공식적으로 지원하는 Terraform 이라는 Provider를 사용하면 이를 구현할 수 있다.

 

The terraform_remote_state Data Source | Terraform | HashiCorp Developer

Retrieves the root module output values from a Terraform state snapshot stored in a remote backend.

developer.hashicorp.com

 

예전에는 Terraform Registry에 Provider에 Terraform이 있었는데 지금은 위 문서로 대체된듯하다. 여튼 위 문서를 참조하면 된다.

 

이 terraform_remote_state의 핵심은 각 워크스페이스 별 상태관리 파일이 있음을 이제 알고 있다. .tfstate 파일로 된. 이 파일을 참조해서 다른 워크스페이스에서 이 상태를 가져다가 사용하겠다는 것이 terraform_remote_state의 핵심이다.

 

근데 이 때 Backend가 무엇이냐에(local, remote) 따라 작성 방식이 살짝 다르다. 이 내용도 위 문서에 있다.

 

The terraform_remote_state Data Source | Terraform | HashiCorp Developer

Retrieves the root module output values from a Terraform state snapshot stored in a remote backend.

developer.hashicorp.com

 

 

The terraform_remote_state Data Source | Terraform | HashiCorp Developer

Retrieves the root module output values from a Terraform state snapshot stored in a remote backend.

developer.hashicorp.com

 

그럼 이제 코드 구조를 한번 살펴보자. 구조는 다음과 같다.

 

설명을 하자면 network 라는 워크스페이스가 만들어내는 리소스가 .tfstate 파일에 저장되어 그 파일을 바라보고 ec2 워크스페이스가 사용할 것이다.

 

network/main.tf

provider "aws" {
  region = "ap-northeast-2"
}

variable "vpc_name" {
  description = "생성되는 VPC의 이름"
  type        = string
}

locals {
  common_tags = {
    Project = "Network"
    Owner   = "cwchoiit"
  }
}

output "vpc_name" {
  value = module.vpc.name
}

output "vpc_id" {
  value = module.vpc.id
}

output "vpc_cidr" {
  description = "생성된 VPC의 CIDR 영역"
  value       = module.vpc.cidr_block
}

output "subnet_groups" {
  value = {
    public  = module.subnet_group__public
    private = module.subnet_group__private
  }
}

module "vpc" {
  source  = "tedilabs/network/aws//modules/vpc"
  version = "0.24.0"

  name       = var.vpc_name
  cidr_block = "10.0.0.0/16"

  internet_gateway_enabled = true

  dns_hostnames_enabled = true
  dns_support_enabled   = true

  tags = local.common_tags
}

module "subnet_group__public" {
  source  = "tedilabs/network/aws//modules/subnet-group"
  version = "0.24.0"

  name                    = "${module.vpc.name}-public"
  vpc_id                  = module.vpc.id
  map_public_ip_on_launch = true

  subnets = {
    "${module.vpc.name}-public-001/az1" = {
      cidr_block           = "10.0.0.0/24"
      availability_zone_id = "apne2-az1"
    }
    "${module.vpc.name}-public-002/az2" = {
      cidr_block           = "10.0.1.0/24"
      availability_zone_id = "apne2-az2"
    }
  }

  tags = local.common_tags
}

module "subnet_group__private" {
  source  = "tedilabs/network/aws//modules/subnet-group"
  version = "0.24.0"

  name                    = "${module.vpc.name}-private"
  vpc_id                  = module.vpc.id
  map_public_ip_on_launch = false

  subnets = {
    "${module.vpc.name}-private-001/az1" = {
      cidr_block           = "10.0.10.0/24"
      availability_zone_id = "apne2-az1"
    }
    "${module.vpc.name}-private-002/az2" = {
      cidr_block           = "10.0.11.0/24"
      availability_zone_id = "apne2-az2"
    }
  }

  tags = local.common_tags
}

이 파일은 기존에 계속 사용했던 network 테라폼 소스이다.

 

network/terraform.tfvars

vpc_name = "remote-state"

 

이렇게 두 개의 파일을 가지는 network 워크스페이스를 Apply 해보자. 정상적으로 수행이 잘 됐다.

 

그럼 이제 주의깊게 봐야하는 파일이 이 Apply를 하고 난 후 생성되는 .tfstate 파일이다.

 

이 파일을 바라보게 EC2 워크스페이스가 작업이 될 것이다.

 

ec2/main.tf

provider "aws" {
  region = "ap-northeast-2"
}

data "terraform_remote_state" "network" {
  backend = "local"

  config = {
    path = "${path.module}/../network/terraform.tfstate"
  }
}

locals {
  vpc_name      = data.terraform_remote_state.network.outputs.vpc_name
  subnet_groups = data.terraform_remote_state.network.outputs.subnet_groups
}

data "aws_ami" "ubuntu" {
  most_recent = true

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }

  owners = ["099720109477"] # Canonical
}

resource "aws_instance" "terraform-ec2" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  subnet_id     = local.subnet_groups["public"].ids[0]

  tags = {
    Name = "${local.vpc_name}-ubuntu"
  }
}

 

여기서 주의깊게 볼 부분은 이 부분이다.

data "terraform_remote_state" "network" {
  backend = "local"

  config = {
    path = "${path.module}/../network/terraform.tfstate"
  }
}

locals {
  vpc_name      = data.terraform_remote_state.network.outputs.vpc_name
  subnet_groups = data.terraform_remote_state.network.outputs.subnet_groups
}

우선, terraform_remote_state를 Data Source로 사용한다. backend는 Local이고 그 경로는 이 파일의 상위 경로에 있는 network 워크스페이스의 .tfstate 파일이 된다.

그리고 로컬 변수로 vpc_name과 subnet_groups를 만들어주는데 이 각각의 변수값을 위에서 선언한 terraform_remote_state의 DataSource를 사용한다. 다만 여기서 한가지 주의할 점은 remote_state를 가져올 땐 .outputs 을 통해서 가져와야한다.

data.terraform_remote_state.network.outputs.vpc_name 이런식으로 말이다.

 

그리고 이 로컬 변수를 EC2 리소스에서도 사용한다. 하단 소스를 보면 subnet_id와 tags의 Name을 로컬변수를 가지고 사용했다.

resource "aws_instance" "terraform-ec2" {
  ami           = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  subnet_id     = local.subnet_groups["public"].ids[0]

  tags = {
    Name = "${local.vpc_name}-ubuntu"
  }
}

 

이 EC2 워크스페이스를 Apply해보자. 다음과 같이 정상적으로 끝난다.

 

이제 AWS Console에서 확인해보자. 우선 VPC, Subnets을 먼저 확인해보자.

 

만든 remote-state가 prefix로 붙은 VPC와 Subnets이 잘 만들어졌고 이 state를 사용한 EC2가 다음과 같이 잘 만들어졌다.

 

 

결론

이렇게 terraform_remote_state를 활용해서 다른 워크스페이스의 state를 가져와 참조하는 기능을 사용해보았다.

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Packer Part. 1  (0) 2024.03.14
Terraform Provisioner/EC2 Userdata  (2) 2024.03.11
Terraform 나만의 Module 만들어보기  (0) 2024.03.10
Terraform Cloud  (0) 2024.03.10
Terraform Workspace  (0) 2024.03.08
728x90
반응형
SMALL
SMALL

저번 시간에는 Terraform Registry에 있는 모듈을 가져다가 사용하는 것 까지는 해봤다. 이번에는 내가 스스로 모듈을 만들어서 그 모듈을 사용해보자.

 

Directory path

우선, 나의 모듈을 가지고 사용하는 테라폼 소스 코드의 구조는 다음과 같다.

 

account 라는 폴더가 하나의 모듈로서 기능을 할 것이고 그 모듈을 루트 디렉토리에 있는 main.tf 에서 사용할 것이다. 그럼 account 디렉토리에 각각의 파일을 하나씩 알아보자.

 

참고로 파일명은 정해져 있는게 아니라 관습적으로 사용되는 명칭이다. 그니까 꼭 저것을 따르지 않아도 되지만 관습이니 안 따라야할 이유도 없다.

 

versions.tf

이 파일은 테라폼과 프로바이더의 버전을 명시한 파일이다. 소스 코드는 다음과 같다.

terraform {
  required_version = ">= 0.15"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = ">= 3.45"
    }
  }
}

 

variables.tf

이 파일은 모듈에서 사용하는 변수를 정의한 파일이다. 소스 코드는 다음과 같다.

variable "name" {
  description = "The name for the AWS account. Used for the account alias."
  type        = string
}

variable "password_policy" {
  description = "Password Policy for the AWS account."
  type = object({
    minimum_password_length        = number
    require_numbers                = bool
    require_symbols                = bool
    require_lowercase_characters   = bool
    require_uppercase_characters   = bool
    allow_users_to_change_password = bool
    hard_expiry                    = bool
    max_password_age               = number
    password_reuse_prevention      = number
  })
  default = {
    minimum_password_length        = 8
    require_numbers                = true
    require_symbols                = true
    require_lowercase_characters   = true
    require_uppercase_characters   = true
    allow_users_to_change_password = true
    hard_expiry                    = false
    max_password_age               = 0
    password_reuse_prevention      = 0
  }
}

 

outputs.tf

이 파일은 모듈에서 만들어내는 outputs을 정의한 파일이다. 특정 테라폼 소스에서 어떤 모듈을 가져다가 사용할 때 모듈이 내뱉는 attribute를 참조해야 하는 경우가 많은데 그 참조할 수 있는 값들은 모듈에서 뱉어내는 output 뿐이다. 그래서 적절한 output이 중요하다.

소스 코드는 다음과 같다.

output "id" {
  description = "The AWS Account ID"
  value       = data.aws_caller_identity.this.account_id
}

output "name" {
  description = "Name of the AWS account. The account alias."
  value       = aws_iam_account_alias.this.account_alias
}

output "signin_url" {
  description = "The URL to signin for the AWS account."
  value       = "https://${var.name}.signin.aws.amazon.com/console"
}

output "password_policy" {
  description = "Password Policy for the AWS Account. `expire_passwords` indicates whether passwords in the account expire. Returns `true` if `max_password_age` contains a value greater than 0."
  value       = aws_iam_account_password_policy.this
}

 

main.tf

모듈의 main.tf 파일이다. 소스 코드는 다음과 같다.

data "aws_caller_identity" "this" {}

resource "aws_iam_account_alias" "this" {
  account_alias = var.name
}

resource "aws_iam_account_password_policy" "this" {
  minimum_password_length        = var.password_policy.minimum_password_length
  require_numbers                = var.password_policy.require_numbers
  require_symbols                = var.password_policy.require_symbols
  require_lowercase_characters   = var.password_policy.require_lowercase_characters
  require_uppercase_characters   = var.password_policy.require_uppercase_characters
  allow_users_to_change_password = var.password_policy.allow_users_to_change_password
  hard_expiry                    = var.password_policy.hard_expiry
  max_password_age               = var.password_policy.max_password_age
  password_reuse_prevention      = var.password_policy.password_reuse_prevention
}

 

README.md

이 파일은 이 모듈이 어떤 형식으로 만들어지고 무엇이 필수값이고 아닌지 또는 버전은 어떻게 되는지 이 모듈의 outputs은 무엇인지를 명시한 파일이다. 그리고 이 파일은 직접 작성한 게 아니고 terraform-docs 라는 툴을 사용했다.

 

terraform-docs

Generate Terraform modules documentation in various formats

terraform-docs.io

이 녀석을 다운받고 시키는 대로 하면 다음과 같이 날 위해 README 파일을 만들어준다.

$ terraform-docs markdown path/to/module

## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 0.15 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 3.45 |

## Providers

| Name | Version |
|------|---------|
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 3.45 |

## Modules

No modules.

## Resources

| Name | Type |
|------|------|
| [aws_iam_account_alias.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_alias) | resource |
| [aws_iam_account_password_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_password_policy) | resource |
| [aws_caller_identity.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_name"></a> [name](#input\_name) | The name for the AWS account. Used for the account alias. | `string` | n/a | yes |
| <a name="input_password_policy"></a> [password\_policy](#input\_password\_policy) | Password Policy for the AWS account. | <pre>object({<br>    minimum_password_length        = number<br>    require_numbers                = bool<br>    require_symbols                = bool<br>    require_lowercase_characters   = bool<br>    require_uppercase_characters   = bool<br>    allow_users_to_change_password = bool<br>    hard_expiry                    = bool<br>    max_password_age               = number<br>    password_reuse_prevention      = number<br>  })</pre> | <pre>{<br>  "allow_users_to_change_password": true,<br>  "hard_expiry": false,<br>  "max_password_age": 0,<br>  "minimum_password_length": 8,<br>  "password_reuse_prevention": 0,<br>  "require_lowercase_characters": true,<br>  "require_numbers": true,<br>  "require_symbols": true,<br>  "require_uppercase_characters": true<br>}</pre> | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_id"></a> [id](#output\_id) | The AWS Account ID |
| <a name="output_name"></a> [name](#output\_name) | Name of the AWS account. The account alias. |
| <a name="output_password_policy"></a> [password\_policy](#output\_password\_policy) | Password Policy for the AWS Account. `expire_passwords` indicates whether passwords in the account expire. Returns `true` if `max_password_age` contains a value greater than 0. |
| <a name="output_signin_url"></a> [signin\_url](#output\_signin\_url) | The URL to signin for the AWS account. |

 

그리고 이제 모듈을 가져다가 사용하는 루트 디렉토리에 있는 main.tf 파일을 보자.

 

main.tf

하단 소스 코드를 보자.

provider "aws" {
  region = "ap-northeast-2"
}

module "account" {
  source = "./account"

  name = "cwchoiit-terraform"
  password_policy = {
    minimum_password_length        = 8
    require_numbers                = true
    require_symbols                = true
    require_lowercase_characters   = true
    require_uppercase_characters   = true
    allow_users_to_change_password = true
    hard_expiry                    = false
    max_password_age               = 0
    password_reuse_prevention      = 0
  }
}

output "id" {
  value = module.account.id
}

output "account_name" {
  value = module.account.name
}

output "signin_url" {
  value = module.account.signin_url
}

output "account_password_policy" {
  value = module.account.password_policy
}

 

저번에는 Registry에 있는 module을 사용할 때 source 형태가 저런식이 아니었다. 그리고 이건 로컬에 있는 모듈을 사용할 때 쓰는 source 형식이다. 이 두 차이점을 잘 알아둬야한다.

 

그리고 account 라는 모듈이 필요한 속성인 name, password_policy 값을 작성한다.

output은 모듈이 주는 output을 그대로 가져다가 사용했다.

 

이 상태에서 Apply를 해보자. 다음과 같이 Apply는 정상적으로 됐다. 

 

그럼 내 계정의 Alias가 저렇게 변경됐는지 확인해보자. 다음은 그 결과이다.

 

 

결론

이렇게 로컬에서 모듈을 직접 만들어보고 사용해보았다. 모듈은 이런식으로 로컬에서도 사용가능하고 Registry에서 가져다가 사용하는 것도 된다. 그리고 한 가지 더 유용한 `terraform-docs`도 사용해봤다.

 

728x90
반응형
LIST

'IaC(Infrastructure as Code)' 카테고리의 다른 글

Terraform Provisioner/EC2 Userdata  (2) 2024.03.11
terraform_remote_state  (0) 2024.03.10
Terraform Cloud  (0) 2024.03.10
Terraform Workspace  (0) 2024.03.08
Terraform Commands (taint / untaint)  (0) 2024.03.08

+ Recent posts