Quantcast
Channel: Outsider's Dev Story
Viewing all 843 articles
Browse latest View live

Terraform으로 관리하던 리소스를 다른 Terraform으로 이동하기

$
0
0

Terraform을 사용하다 보면 리소스를 terraform으로 관리하다가 분리해야 하는 경우가 생긴다. 기본적으로 Terraform은 모든 리소스는 하나의 Terraform으로 관리하기보다는 여러 terraform 프로젝트로 나누어서 관리하는 것을 더 권장한다. 실제로 많은 리소스를 하나에 담아버리면 관리도 쉽지 않고 plan 등을 하는데도 너무 많은 시간이 걸린다.

하지만 처음부터 모든 상황을 예상하기는 어려우므로 처음에는 A 프로젝트에서 관리하고 있었는데 작업을 하다 보니 B 프로젝트에서 관리하는 게 낫다고 싶을 때도 있다. 이때 terraform 설정은 옮겨가기가 쉽지만 terraform.tfstate에서도 빼주지 않으면 설정을 제거하고 apply하면 실제 리소스도 제거하므로 문제가 생기게 된다. terraform.tfstate는 기본적으로 사람이 직접 관리하는 용도는 아니므로(어쩔 수 없는 경우가 아니라면...) terraform.tfstate를 직접 관리하는 건 쉽지 않다.

설명을 위해서 상황을 가정해 보자. AWS의 IAM role을 다음과 같이 관리하고 있다고 해보자.

resource "aws_iam_role" "ecsInstanceRole" {
    name               = "ecsInstanceRole"
    path               = "/"
    assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_role" "nodejs-ko-twitter_lambda_function" {
    name               = "nodejs-ko-twitter_lambda_function"
    path               = "/"
    assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

resource "aws_iam_role" "sideeffect_lambda_function" {
    name               = "sideeffect_lambda_function"
    path               = "/"
    assume_role_policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
POLICY
}

여기 3개의 role이 있고 이는 이미 terraform apply를 했으므로 Terraform이 관리하는 리소스이다. 이렇게 관리하고 있다가 sideeffect_lambda_function role은 다른 프로젝트에서 관리하는 게 더 나을 것 같아서 여기서는 제거하고 빼고 싶다고 해보자.

이 경우 위 .tf파일에서 설정도 제거하고 terraform.tfstate에서도 제거해야 terraform plan을 했을 때 아무런 영향이 없게 된다.

terraform state rm

이런 경우를 위해서 제공되는 명령어가 state rm이다. 이 명령어를 이용하면 상태 파일(terraform.tfstate)에서 리소스를 제거할 수 있다. 제거할 때는 리소스의 이름을 사용하는데 여기서는 aws_iam_role.sideeffect_lambda_function가 된다. 어떤 이름을 써야 하는지 헷갈린다면 먼저 .tf파일에서 설정을 제거하고 terraform plan을 때리면 제거할 리소스가 나오게 되는데 이 이름을 그대로 사용하면 된다.

$ terraform plan

- aws_iam_role.sideeffect_lambda_function


Plan: 0 to add, 0 to change, 1 to destroy.

위에서 나온 이름을 사용하면 된다.

$ terraform state rm aws_iam_role.sideeffect_lambda_function
Item removal successful.

정상적으로 삭제된 것을 확인할 수 있고 이제 plan을 실행해도 No changes. Infrastructure is up-to-date.가 나왔으므로 제대로 제거됐다. 이동할 다른 프로젝트에서는 AWS에 기존에 있는 리소스를 가져오는 것이므로 기존에 사용 중인 인프라를 Terraform으로 가져오기를 참고하면 된다.

댓글 쓰기


기술 뉴스 #81 : 17-07-02

$
0
0

웹개발 관련

  • Unlocking Test Performance — Migrating from Mocha to Jest : Airbnb에서 기존 프론트엔드 테스트 코드로 Mocha를 쓰다가 Jest로 변경한 결과를 공유한 글이다. 기존에 12분 이상 걸리던 테스트(로컬에서는 45분 정도)가 4분 30초 정도로 줄어들었고 병렬로 테스트를 실행하는 Jest의 특징을 설명하고 병렬로 테스트를 수행하기 위해 각 테스트를 격리하기 위해서 작업한 과정까지 설명하고 있다.(영어)
  • High Performance JS in V8 : V8 엔진에 기존의 Crankshaft에서 최근에 Ignition + Trubofan으로 변경되면서 어떻게 동작하고 성능 개선이 얼마나 이뤄졌는지를 정리한 발표자료이다. 글로 설명된 내용보다 좀 더 간단하게 이해하기 쉽다.(영어)
  • The State of Angular and the Due Date of Version 5 : Angular 프로젝트의 현재 상태와 차기 버전의 릴리스 일정을 설명하는 글이다. 왜 Angular 4.0을 만들게 되었고 4.x의 버전에서 어떤 기능이 추가되었으며 다음 버전인 5.0은 올해 9월에 릴리스 될 예정이라고 밝히고 있다. 5.0에서는 AOT가 기본이 되고 watch 모드, 템플릿의 타입 검사 등이 추가될 예정이다.(영어)
  • Learning Draft Js : React 용 텍스트 에디터 프레임워크인 Draft.js를 이용해서 웹에 텍스트 에디터를 구현하는 방법을 설명한 글이다. 초기 설정부터 서버 연동 등까지 설명하면서 예제 코드도 함께 제공하고 있다.(영어)
  • 웹 풀스택 입문을 위한 약 500페이지 분량의 교재를 무료로 배포하고 있습니다. : 벤젠에서 웹 풀스택을 공부할 수 있는 500페이지 정도의 교재를 PDF로 배포하고 있다. 이 자료에는 기본적인 프로그래밍부터 Node.js로 웹 사이트를 만들어서 구축하는 부분까지 정리되어 있다.(한국어)
  • Getting Started With WebAssembly : WebAssembly를 이용해서 C로 작성한 코드를 Emscripten 컴파일러로 컴파일한 후 웹 브라우저에서 불러와서 사용하는 과정을 설명한 글이다.(영어)

그 밖의 프로그래밍 관련

  • State Drift Detection using Terraform : AWS 인프라를 Terraform으로 관리하면서 수동으로 변경하거나 의도치 않은 변경이 발생했을 때 인지할 수 있도록 Jenkins에서 terraform plan을 정기적으로 실행해서 인프라 변경이 있으면 알림을 받도록 접근한 내용을 설명한 글이다.(영어)
  • Node.js Performance Monitoring with Prometheus : Node.js 성능 모니터링 도구를 만드는 회사인 RisingStack에서 Node.js 애플리케이션을 Prometheus로 모니터링하는 방법을 소개하는 글이다. 모니터링을 할 때 추적해야 하는 정보와 도구를 선택할 때 고려해야할 부분을 설명하고 Prometheus를 Node.js와 연동해서 어떻게 모니터링하고 알림을 받는지 설명하고 있다.(영어)
  • git-tips 한국어 : git-tips의 번역 문서로 git을 사용할 때 상황별로 유용한 팁들이 정리되어 있다.(한국어)

볼만한 링크

  • Redesigning Google News for everyone : Google News가 디자인을 개편하면서 기존과 비교하면서 어떤 점을 바꾸었고 왜 바꾸었는지를 설명한 글이다. 전체적인 변경을 비교해서 보여주니 얼마나 가독성이 좋아졌는지 이해하기 쉽고 각 섹션별로 의도한 부분을 설명해주기 때문에 유용한 정보가 많이 담겨있다.(영어)
  • 리디북스 이벤트 디자인에 숨겨진 비밀 : 리디북스의 서비스를 좋아하는 편인데 이 글에서는 리디북스에서 이벤트 페이지를 만들 때 사용자에게 정보를 잘 전달하기 위해서 사용자의 시선의 흐름과 공백을 이용하는 방법 등 디자인에서 신경 쓰는 부분을 설명하고 있다.(한국어)

IT 업계 뉴스

프로젝트

  • react-flight : React 용 애니메이션 컴포넌트.
  • decaffeinate : CoffeeScript를 JavaScript로 바꿔주는 도구.
  • HNPWA : React, Vue 등 다양한 프레임워크로 HackerNews 리더를 PWA으로 구현해서 예제로 공유하는 페이지.
  • TensorFlow Tutorials : TensorFlow를 기초부터 연습해 볼 수 있는 소스코드가 공유된 저장소로 골빈해커님이 작성해서 소스코드의 주석도 모두 한글로 되어 있다.

버전 업데이트

댓글 쓰기

Docker v17.06.0-ce에 도입된 multi-stage 빌드 사용하기

$
0
0

지난주에 Docker CE v17.06.0-ce가 나왔다. 이 v17.06.0-ce에는 Dockercon 2017에서 발표된 multi-stage 빌드가 포함되어 있다. 이제 정식 버전에서 실제로 사용할 수 있게 되었다.

multi-stage 빌드가 없던 환경

multi-stage 빌드는 컨테이너 이미지를 만들면서 빌드 등에는 필요하지만 최종 컨테이너 이미지에는 필요 없는 환경을 제거할 수 있도록 단계를 나누어서 기반 이미지를 만드는 방법을 얘기한다. 실제로 Docker를 사용하다 보면 불필요하게 컨테이너 이미지가 커지지만, 기존에는 multi-stage 빌드를 지원하지 않으므로 어쩔 수 없이 감수해야 하는 경우가 있다.

multi-stage 빌드를 이해하기 전에 기존에 Docker 이미지를 만드는 방법을 살펴보기 위해 다음 Dockerfile을 살펴보자.

FROM golang:1.8.3
MAINTAINER Outsider

ENV VAULT_VERSION=0.7.3

## clone vault source code
WORKDIR /go/src/github.com/hashicorp
RUN git clone https://github.com/hashicorp/vault.git

## build vault
WORKDIR /go/src/github.com/hashicorp/vault
RUN git checkout v"${VAULT_VERSION}"
RUN make bootstrap
RUN make dev

RUN mv /go/src/github.com/hashicorp/vault/bin/vault /bin/

CMD ["vault", "server", "-dev"]

이 이미지는 Vault를 빌드해서 사용하는 Docker 이미지이다.(Vault에 대해서 궁금하다면 이전 글참고.) Vault는 OS별로 미리 빌드된 파일을 제공하지만 여기서는 직접 빌드해서 사용한다고 해보자. 여기선 예시일 뿐이지만 필요에 따라 직접 빌드해서 사용해야 하는 경우도 있고 요즘 JavaScript 같은 경우 트랜스파일을 하기 위해 Babel 등을 설치해서 js 파일을 변환해야 하는 경우가 있다.

Dockerfile의 경우 최종 컨테이너 이미지에서 필요한 것은 빌드가 완료된 vault파일 뿐이지만 golang환경도 포함되어 있고 Vault 소스코드도 들어있다. Docker에서 이런 상황이 필요하다면 이는 어쩔 수 없는 부분이고 이를 피하려면 별도로 vault바이너리를 빌드한 후에 Docker 이미지를 만들 때 ADD로 추가해야 한다. 물론 이러면 이 바이너리 파일을 형상관리 도구 등에서 별도로 관리해야 한다.

여기서는 기반 이미지를 golang:1.8.3를 사용하고 있고 이 이미지는 각종 개발에 필요한 의존성이 포함된 buildpack-deps:jessie-scm에 기반을 두고 있다.

$ docker images
REPOSITORY       TAG          IMAGE ID       CREATED       SIZE
buildpack-deps   jessie-scm   46157f071d19   12 days ago   291MB
golang           1.8.3        d2f558dda133   10 days ago   699MB

이 이미지를 보면 buildpack-deps:jessie-scm은 291MB인데 golang:1.8.3은 699MB이다. 이를 기반으로 앞에서 만든 Dockerfiledocker build -t oustider-example:0.1 .를 실행해서 이미지를 만들어 보자.

$ docker images
REPOSITORY         TAG   IMAGE ID       CREATED          SIZE
oustider-example   0.1   e0f440079a74   19 minutes ago   1.04GB

이제 용량이 1.04GB나 되었다. 단순히 Vault를 직접 빌드해서 사용하고자 했을 뿐인데 최종 컨테이너 이미지는 엄청나게 커졌다. 동작에는 문제가 없지만 배포하거나 할 때 시간도 오래 걸리고 불필요한 트래픽을 유발하게 된다.

Docker의 multi-stage 빌드

앞에서는 golang:1.8.3이미지를 사용했지만, 이는 실제로는 buildpack-deps:jessie-scm에 기반을 두고 있다. 여기에는 빌드를 위한 의존성이 포함되어 있으므로 최종 이미지는 debian:jessie기반으로 만드는 정도로 충분한데 이 이미지는 123MB의 용량을 가진다.

앞에서 사용한 Dockerfile을 multi-stage 빌드를 사용하도록 변경해 보자.

# bulid stage
FROM golang:1.8.3 AS build
MAINTAINER Outsider

ENV VAULT_VERSION=0.7.3

## clone vault source code
WORKDIR /go/src/github.com/hashicorp
RUN git clone https://github.com/hashicorp/vault.git

## build vault
WORKDIR /go/src/github.com/hashicorp/vault
RUN git checkout v"${VAULT_VERSION}"
RUN make bootstrap
RUN make dev

# final stage
FROM debian:jessie
MAINTAINER Outsider

## copy vault from build
COPY --from=build /go/src/github.com/hashicorp/vault/bin/vault /bin/

CMD ["vault", "server", "-dev"]

위 파일에는 기존과 다른 점이 있다.

  • FROM이 2번 존재한다. 앞에서는 vault를 빌드하는 환경이고 두 번째 FROM이 최종 이미지를 만드는 부분이다.
  • FROM에는 FROM golang:1.8.3 AS build처럼 별칭을 지정했다. 이는 나중에 사용하기 위함이다.
  • 최종 이미지를 만들기 위해서 FROM debian:jessie을 사용했다.
  • COPY --from=build /go/src/github.com/hashicorp/vault/bin/vault /bin/처럼 앞에서 build로 지정한 환경에서 파일을 가져와서 최종 이미지에 파일을 추가한다.

사용방법을 보다시피 아주 간단하다. 이를 통해서 이미지를 만드는 데 필요한 의존성과 실제 최종 이미지에서만 필요한 의존성을 완전히 분리해서 최종 Docker 이미지를 아주 간단하게 만들 수 있다.

$ docker images
REPOSITORY         TAG   IMAGE ID       CREATED          SIZE
oustider-example   0.1   c03283b723ca   10 seconds ago   183MB

아까와 똑같이 Vault를 직접 빌드해서 Docker 이미지를 만들었지만, 최종 이미지는 183MB에 불과하게 됐다. 물론 동작은 둘 다 똑같이 잘 동작한다.

그리고 테스트해 본 결과 이 최종 이미지는 그냥 Docker 이미지이므로 이 이미지를 실행할 Docker도 17.06 이상일 필요는 전혀 없다. multi-stage 빌드를 위해서 Docker 이미지를 만들 때만 v17.06가 필요할 뿐이다.

댓글 쓰기

Terraform으로 AWS VPC 생성하기

$
0
0

회사에서 Terraform을 계속 사용하고 있어서 연습 겸 개인 AWS 인프라도 모두 Terraform으로 최근에 갈아탔다. 어차피 새로 구성하는 것이므로 큰 부담 없이 테스트로 사용했다.

VPC

VPC는 Virtual Private Cloud를 의미한다. AWS에서 EC2 서버나 다른 리소스를 사용하려면 먼저 VPC를 생성해야 한다. 이 VPC로 AWS 내에서 사용할 내부 CIDR대역을 지정하고 내부 네트워크를 구성할 수 있다. 이렇게 만든 VPC 내에서 외부에 점근 등을 제어할 수 있다. 이 글은 VPC의 개념까지 설명하기에는 어려우므로 AWS 문서AWS VPC를 디자인해보자를 읽어보자. 나 같은 경우에는 후자의 글이 꽤 많은 도움이 되었다.

Terraform으로 구성할 VPC

여기서는 Terraform으로 VPC를 구성하는 방법을 설명한다. Terraform으로 AWS 등의 리소스를 구성하는 것은 문서를 찾아가면서 손수 정의해야 하므로 꽤 고통스러운 작업이지만 Terraform으로 구성을 해보면 AWS 리소스에 대한 이해도가 아주 높아지는 것을 느낄 수 있다. API 등을 이용할 때도 비슷하겠지만 AWS 웹 콘솔에서 VPC를 만들면 그 개념을 잘 몰라도 GUI가 안내를 해주거나 빠진 부분을 설명해 주무로 대충 만지다 보면 구성할 수 있다.

Terraform으로 구성하려면 웹 콘솔보다 훨씬 세부적인 리소스로 나누어지고 각 리소스 간의 관계를 직접 지정해 주어야 하므로 서로 어떤 관계를 갖는지 이해도가 없으면 만들기가 어렵다. 그래서 Terraform으로 직접 리소스를 정의하다 보면 각 리소스에 대한 용도와 관계를 어쩔 수 없이 공부하게 되고 자연히 이해도가 높아진다. 이전 Terraform으로 AWS 관리하기에서 간단한 VPC를 만들어서 예시로 사용했지만, 실제 서비스하는 VPC를 만들려면 상당히 많은 리소스가 필요하다. VPC를 어떻게 구성하는가는 다양한 접근 방법이 가능하지만 가장 많이 사용하는 시나리오가 AWS 문서에 잘 나와 있다.

VPC는 직접 구성하는 것이므로 필요에 따라 원하는 대로 만들 수 있다. 여기서 만들 VPC는 AWS 문서에 나와 있는 퍼블릭 서브넷과 프라이빗 서브넷이 있는 VPC(NAT)의 구조와 가장 가까운 형태이고 특별한 요구사항이 없다면 가장 일반적인 구조라고 생각한다.

VPC 구성도

정확한 형태를 그리면 위와 같은 형태가 된다. 이 그림에도 많은 개념이 들어가 있는데 이글에서 모두 설명하기는 쉽지 않다. Terraform으로 구성하기 전에 VPC에 대해 어느 정도 이해를 하고 있어야 하므로 간단히 특징 부분만 설명한다.

  • Region 안에는 Availability Zone(AZ)이 여러 개 있다. 여기서는 2개를 사용하는데 보통 이중화를 할 때 다른 AZ에 같은 서버 및 구성을 두어 한쪽 AZ에 장애가 나더라도 문제없게 한다. 그래서 이 그림에서도 양쪽에 두 개의 AZ가 있다.
  • VPC를 생성하고 모든 자원은 이 VPC 안에 만든다.
  • VPC안에 퍼블릭 서브넷과 프라이빗 서브넷을 2개씩 만든다. 2개인 이유는 AZ마다 하나씩 만들기 때문이다.

    • 퍼블릭 서브넷은 외부에서 접근할 수 있고 내부에서도 VPC 밖의 인터넷으로 접근할 수 있는 서브넷이다. 퍼블릭 IP로 접근해야 하는 서버 등은 여기에 띄워야 한다.
    • 프라이빗 서브넷은 외부에서는 접근할 수 없고 VPC 내에서만 접근할 수 있다. 대부분의 서비스 서버는 여기에 둔다.
  • 퍼블릭 서브넷

    • 서브넷앞에 Network ACL을 둔다. Network ACL로 오가는 트래픽을 모두 제어할 수 있다.
    • Network ACL 앞에 Route Table을 둔다. 이는 Subnet 내의 트래픽을 어디로 갈지 정하는데 VPC의 CIDR은 모두 내부를 보도록 하고 외부로 나가는 트래픽은 Internet Gateway로 보내도록 한다.
    • Internet Gateway를 통해서 퍼블릭 서브넷의 아웃바운드 트래픽이 외부 인터넷으로 연결된다.
  • 프라이빗 서브넷

    • 이 서브넷에 RDS나 EC2 인스턴스를 둔다. 서버 자체는 외부에서 아예 접근이 안 되고 서비스는 ELB를 통해서 공개하므로 여기에 두는 것이 좋다.
    • 똑같이 서브넷 앞에 Network ACL과 Route Table을 둔다.
    • 프라이빗 서브넷은 외부에서 접근할 수 있지만 서브넷 내에서 외부에 접근은 가능해야 한다. 패키지를 설치하거나 소스를 가져오거나... 기본적으로 프라이빗 서브넷은 막혀 있으므로 Route Table로 아웃바운드 트래픽을 NAT Gateway로 연결한다.
    • NAT Gateway도 AWS에서 서비스로 제공하는데 이를 퍼블릭 서브넷 안에 만들어 두고 프라이빗 서브넷의 아웃바운드 트래픽은 이 NAT Gateway를 통해서 외부 인터넷으로 나가게 된다.
  • Bastion host

    • 바스티온 호스트는 네트워크에 접근하기 위한 서버를 의미한다.
    • VPC 자체를 네트워크단에서 접근을 제어하고 있으므로 퍼블릭 서브넷에 바스티온 호스트를 만들어두고 외부에서 SSH 등으로 접근할 수 있는 서버는 이 서버가 유일하다.
    • 프라이빗 서브넷이나 VPC 내의 자원에 접근하려면 바스티온 호스트에 접속한 뒤에 다시 접속하는 방식으로 사용한다.
    • Bastion Host도 이중화해서 AZ마다 한대씩 만들어 둘 수 있다.

Terraform으로 VPC 구성하기

Terrafrom으로 VPC를 구성해 보자.

resource "aws_vpc" "side_effect" {
  cidr_block  = "10.10.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support = true
  instance_tenancy = "default"

  tags {
    "Name" = "side effect"
  }
}

aws_vpc리소스로 VPC를 정의한다. 여기서 side_effect라는 이름은 임의로 준 이름이다.(내 개인 프로젝트용 도메인이이라서...) 리소스 이름은 맘대로 사용할 수 있는데 나중에 aws_vpc.side_effect.id, aws_subnet,side_effect.id같은 식으로 참조해서 사용하므로 같은 리소스 내에서만 이름이 충돌하지 않으면 된다. 그래서 여러 이름이 필요하지 않으면 연관된 리소스는 같은 이름을 쓰거나 접두사 형식으로 사용한다. 그리고 Terraform의 리소스 명이 모두 스네이크케이스(_로 단어를 이어붙이는..)를 사용하기 때문에 이름도 같은 방식을 사용하고 있다.

aws_vpc는 VPC를 하나 만든 것이다. 원하는 설정을 넣고 CIDR 대역을 10.10.0.0/16로 지정했다.

resource "aws_default_route_table" "side_effect" {
  default_route_table_id = "${aws_vpc.side_effect.default_route_table_id}"

  tags {
    Name = "default"
  }
}

aws_default_route_table는 좀 특수한 리소스이다. AWS에서 VPC를 생성하면 자동으로 route table이 하나 생긴다. 이는 Terraform으로 직접 생성하는 것이 아니므로 aws_default_route_table는 route table을 만들지 않고 VPC가 만든 기본 route table을 가져와서 Terraform이 관리할 수 있게 한다. 이 Route Table에 이름을 지정하고 관리하게 두기 위해서 Terraform으로 가져왔고 이 테이블과의 연결은 VPC에서 사용하는 다른 속성을 사용할 예정이다. 이는 뒤에서 좀 더 설명한다.

// public subnets
resource "aws_subnet" "side_effect_public_subnet1" {
  vpc_id = "${aws_vpc.side_effect.id}"
  cidr_block = "10.10.1.0/24"
  map_public_ip_on_launch = false
  availability_zone = "${data.aws_availability_zones.available.names[0]}"
  tags = {
    Name = "public-az-1"
  }
}

resource "aws_subnet" "side_effect_public_subnet2" {
  vpc_id = "${aws_vpc.side_effect.id}"
  cidr_block = "10.10.2.0/24"
  map_public_ip_on_launch = true
  availability_zone = "${data.aws_availability_zones.available.names[1]}"
  tags = {
    Name = "public-az-2"
  }
}

서브넷은 aws_subnet으로 만드는데 앞에서 말한 대로 퍼블릭 서브넷을 2개 만들고 각각 CIDR 대역을 지정했다. 그리고 퍼블릭 서브넷이므로 서버 등을 띄울 때 자동으로 퍼블릭 IP가 할당되도록 map_public_ip_on_launch를 지정했다.

availability_zone으로 두 서브넷이 다른 AZ에 생성하도록 했다.

> data.aws_availability_zones.available.names
[
  ap-northeast-1a,
  ap-northeast-1c
]

data "aws_availability_zones" "available" {}와 같은 데이터를 지정하면 아래와 같이 해당 리전의 AZ 이름을 가져올 수 있다. 여기서는 이 배열의 첫 번째와 두 번째를 각각 지정해서 이름에서 오타가 발생하거나 이름을 기억할 필요가 없게 한 것이다.

// private subnets
resource "aws_subnet" "side_effect_private_subnet1" {
  vpc_id = "${aws_vpc.side_effect.id}"
  cidr_block = "10.10.10.0/24"
  availability_zone = "${data.aws_availability_zones.available.names[0]}"
  tags = {
    Name = "private-az-1"
  }
}

resource "aws_subnet" "side_effect_private_subnet2" {
  vpc_id = "${aws_vpc.side_effect.id}"
  cidr_block = "10.10.11.0/24"
  availability_zone = "${data.aws_availability_zones.available.names[1]}"
  tags = {
    Name = "private-az-2"
  }
}

퍼블릭 서브넷과 똑같이 프라이빗 서브넷을 2개 만들었다.

resource "aws_internet_gateway" "side_effect_igw" {
  vpc_id = "${aws_vpc.side_effect.id}"
  tags {
    Name = "internet-gateway"
  }
}

aws_internet_gateway로 VPC에서 외부 인터넷에 접근하기 위한 인터넷 게이트웨이를 만들었다. 인터넷 게이트웨이를 AWS에서 제공하므로 이를 VPC 안에 만들면 된다.

// route to internet
resource "aws_route" "side_effect_internet_access" {
  route_table_id = "${aws_vpc.side_effect.main_route_table_id}"
  destination_cidr_block = "0.0.0.0/0"
  gateway_id = "${aws_internet_gateway.side_effect_igw.id}"
}

aws_route는 Route Table에 라우팅 규칙을 추가하는 리소스이다. 이 테이블을 aws_vpc.side_effect.main_route_table_id로 Route Table에 추가했다. main_route_table_id는 VPC의 기본 Route Table을 의미하고 이는 앞에서 살펴본 aws_default_route_table와 같은 테이블이다. aws_default_route_table.side_effect.id를 사용해도 같은 값이지만 의미가 더 명확해 보이는 main_route_table_id를 사용했다.

// eip for NAT
resource "aws_eip" "side_effect_nat_eip" {
  vpc = true
  depends_on = ["aws_internet_gateway.side_effect_igw"]
}

// NAT gateway
resource "aws_nat_gateway" "side_effect_nat" {
  allocation_id = "${aws_eip.side_effect_nat_eip.id}"
  subnet_id = "${aws_subnet.side_effect_public_subnet1.id}"
  depends_on = ["aws_internet_gateway.side_effect_igw"]
}

프라이빗 서브넷에서 외부 인터넷으로 요청을 내보낼 수 있도록 하는 NAT 게이트웨이다. NAT 게이트웨이에서 사용할 elastic IP를 하나 만들고 aws_nat_gateway에서 이 IP를 연결하고 퍼블릭 서브넷에 만들어지도록 했다. 둘다 인터넷 게이트웨이가 만들어진 뒤에 구성하려고 aws_nat_gateway에 의존성을 지정했다.

// private route table
resource "aws_route_table" "side_effect_private_route_table" {
  vpc_id = "${aws_vpc.side_effect.id}"
  tags {
    Name = "private"
  }
}

resource "aws_route" "private_route" {
  route_table_id = "${aws_route_table.side_effect_private_route_table.id}"
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id = "${aws_nat_gateway.side_effect_nat.id}"
}

프라이빗 서브넷에서 사용할 Route Table을 만들고 여기서 0.0.0.0/0으로 나가는 요청이 모두 NAT 게이트웨이로 가도록 설정했다.

// associate subnets to route tables
resource "aws_route_table_association" "side_effect_public_subnet1_association" {
  subnet_id = "${aws_subnet.side_effect_public_subnet1.id}"
  route_table_id = "${aws_vpc.side_effect.main_route_table_id}"
}

resource "aws_route_table_association" "side_effect_public_subnet2_association" {
  subnet_id = "${aws_subnet.side_effect_public_subnet2.id}"
  route_table_id = "${aws_vpc.side_effect.main_route_table_id}"
}

resource "aws_route_table_association" "side_effect_private_subnet1_association" {
  subnet_id = "${aws_subnet.side_effect_private_subnet1.id}"
  route_table_id = "${aws_route_table.side_effect_private_route_table.id}"
}

resource "aws_route_table_association" "side_effect_private_subnet2_association" {
  subnet_id = "${aws_subnet.side_effect_private_subnet2.id}"
  route_table_id = "${aws_route_table.side_effect_private_route_table.id}"
}

위에서 Route Table을 2개 만들었는데 이를 각각 퍼블릭/프라이빗 서브넷에 연결하는 과정이다. 퍼블릭 서브넷에는 메인 Route Table을 연결하고 Private 용으로 만들 Route Table은 프라이빗 서브넷에 연결했다.

// default security group
resource "aws_default_security_group" "side_effect_default" {
  vpc_id = "${aws_vpc.side_effect.id}"

  ingress {
    protocol  = -1
    self      = true
    from_port = 0
    to_port   = 0
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags {
    Name = "default"
  }
}

aws_default_security_group은 VPC를 만들면 자동으로 만들어지는 기본 시큐리티 그룹을 Terraform에서 관리할 수 있도록 지정한 것이다. 이 리소스는 Terraform이 생성하지 않고 정보만 가져와서 연결한다.

resource "aws_default_network_acl" "side_effect_default" {
  default_network_acl_id = "${aws_vpc.side_effect.default_network_acl_id}"

  ingress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  egress {
    protocol   = -1
    rule_no    = 100
    action     = "allow"
    cidr_block = "0.0.0.0/0"
    from_port  = 0
    to_port    = 0
  }

  tags {
    Name = "default"
  }
}

aws_default_network_acl도 VPC를 만들 때 기본으로 만들어지는 네트워크 ACL이다. 이름을 지정하고 관리하기 위해서 가져왔지만, 이 ACL은 어떻게 설정해서 다뤄야 할지 애매해서 가져온 채로만 두고 그대로 놔두었다. 각 서브넷에서 사용할 네트워크 ACL을 추가로 만들어서 사용했다.

// network acl for public subnets
resource "aws_network_acl" "side_effect_public" {
  vpc_id = "${aws_vpc.side_effect.id}"
  subnet_ids = [
    "${aws_subnet.side_effect_public_subnet1.id}",
    "${aws_subnet.side_effect_public_subnet2.id}",
  ]

  tags {
    Name = "public"
  }
}

퍼블릭 서브넷에서 사용할 네트워크 ACL을 생성해서 퍼블릭 서브넷에 연결했다.

resource "aws_network_acl_rule" "side_effect_public_ingress80" {
  network_acl_id = "${aws_network_acl.side_effect_public.id}"
  rule_number = 100
  rule_action = "allow"
  egress = false
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 80
  to_port = 80
}

resource "aws_network_acl_rule" "side_effect_public_egress80" {
  network_acl_id = "${aws_network_acl.side_effect_public.id}"
  rule_number = 100
  rule_action = "allow"
  egress = true
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 80
  to_port = 80
}

resource "aws_network_acl_rule" "side_effect_public_ingress443" {
  network_acl_id = "${aws_network_acl.side_effect_public.id}"
  rule_number = 110
  rule_action = "allow"
  egress = false
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 443
  to_port = 443
}

resource "aws_network_acl_rule" "side_effect_public_egress443" {
  network_acl_id = "${aws_network_acl.side_effect_public.id}"
  rule_number = 110
  rule_action = "allow"
  egress = true
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 443
  to_port = 443
}

resource "aws_network_acl_rule" "side_effect_public_ingress22" {
  network_acl_id = "${aws_network_acl.side_effect_public.id}"
  rule_number = 120
  rule_action = "allow"
  egress = false
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 22
  to_port = 22
}

resource "aws_network_acl_rule" "side_effect_public_egress22" {
  network_acl_id = "${aws_network_acl.side_effect_public.id}"
  rule_number = 120
  rule_action = "allow"
  egress = true
  protocol = "tcp"
  cidr_block = "${aws_vpc.side_effect.cidr_block}"
  from_port = 22
  to_port = 22
}

resource "aws_network_acl_rule" "side_effect_public_ingress_ephemeral" {
  network_acl_id = "${aws_network_acl.side_effect_public.id}"
  rule_number = 140
  rule_action = "allow"
  egress = false
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 1024
  to_port = 65535
}

resource "aws_network_acl_rule" "side_effect_public_egress_ephemeral" {
  network_acl_id = "${aws_network_acl.side_effect_public.id}"
  rule_number = 140
  rule_action = "allow"
  egress = true
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 1024
  to_port = 65535
}

퍼블릭 서브넷용 네트워크 ACL에 규칙을 추가한 부분이다. 네트워크 ACL을 기본적으로는 모든 포트가 막혀있으므로 필요한 부분을 열어야 하므로 80, 443, 22, ephemeral 포트를 인바운드/아웃바운드로 열어서 규칙을 추가했다.

// network acl for private subnets
resource "aws_network_acl" "side_effect_private" {
  vpc_id = "${aws_vpc.side_effect.id}"
  subnet_ids = [
    "${aws_subnet.side_effect_private_subnet1.id}",
    "${aws_subnet.side_effect_private_subnet2.id}"
  ]

  tags {
    Name = "private"
  }
}

프라이빗 서브넷에서 사용할 네트워크 ACL을 만들어서 서브넷에 연결했다.

resource "aws_network_acl_rule" "side_effect_private_ingress_vpc" {
  network_acl_id = "${aws_network_acl.side_effect_private.id}"
  rule_number = 100
  rule_action = "allow"
  egress = false
  protocol = -1
  cidr_block = "${aws_vpc.side_effect.cidr_block}"
  from_port = 0
  to_port = 0
}

resource "aws_network_acl_rule" "side_effect_private_egress_vpc" {
  network_acl_id = "${aws_network_acl.side_effect_private.id}"
  rule_number = 100
  rule_action = "allow"
  egress = true
  protocol = -1
  cidr_block = "${aws_vpc.side_effect.cidr_block}"
  from_port = 0
  to_port = 0
}

resource "aws_network_acl_rule" "side_effect_private_ingress_nat" {
  network_acl_id = "${aws_network_acl.side_effect_private.id}"
  rule_number = 110
  rule_action = "allow"
  egress = false
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 1024
  to_port = 65535
}

resource "aws_network_acl_rule" "side_effect_private_egress80" {
  network_acl_id = "${aws_network_acl.side_effect_private.id}"
  rule_number = 120
  rule_action = "allow"
  egress = true
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 80
  to_port = 80
}

resource "aws_network_acl_rule" "side_effect_private_egress443" {
  network_acl_id = "${aws_network_acl.side_effect_private.id}"
  rule_number = 130
  rule_action = "allow"
  egress = true
  protocol = "tcp"
  cidr_block = "0.0.0.0/0"
  from_port = 443
  to_port = 443
}

프라이빗 서브넷용 네트워크 ACL을 위한 규칙으로 VPC 내에서는 모든 포트를 열고(더 엄격히 갈 수도 있지만, 개인용이라 좀 편하게 설정했다.) NAT으로 들어오는 요청과 80, 443으로 나가는 요청을 규칙으로 추가해서 열어주었다.

// Basiton Host
resource "aws_security_group" "side_effect_bastion" {
  name = "bastion"
  description = "Security group for bastion instance"
  vpc_id = "${aws_vpc.side_effect.id}"

  ingress {
    from_port = 22
    to_port = 22
    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"]
  }

  tags {
    Name = "bastion"
  }
}

바스티온 호스트에서 사용할 시큐리티 그룹을 하나 만들었다. 바스티온 호스트에 접속 가능한 IP 대역을 지정할 수도 있지만 나는 어디서 접근할지 알 수 없으므로 전체로 22 포트를 여는 시큐리티 그룹을 만들었다.

resource "aws_instance" "side_effect_bastion" {
  ami = "${data.aws_ami.ubuntu.id}"
  availability_zone = "${aws_subnet.side_effect_public_subnet1.availability_zone}"
  instance_type = "t2.nano"
  key_name = "YOUR-KEY-PAIR-NAME"
  vpc_security_group_ids = [
    "${aws_default_security_group.side_effect_default.id}",
    "${aws_security_group.side_effect_bastion.id}"
  ]
  subnet_id = "${aws_subnet.side_effect_public_subnet1.id}"
  associate_public_ip_address = true

  tags {
    Name = "bastion"
  }
}

resource "aws_eip" "side_effect_bastion" {
  vpc = true
  instance = "${aws_instance.side_effect_bastion.id}"
  depends_on = ["aws_internet_gateway.side_effect_igw"]
}

바스티온 호스트를 EC2 인스턴스로 띄웠다. 바스티온 호스트는 중계 역할 밖에 안 하므로 간단히 t2.nano로 퍼블릭 서브넷에 EC2 인스턴스를 띄웠다. 이 인스턴스는 data.aws_ami.ubuntu.id로 우분투 AMI의 ID를 가져와서 사용했는데 원하는 AMI ID를 지정해서 사용해도 된다. 바스티온 호스트의 IP가 서버 바꿀 때마다 바뀌면 피곤하므로 Elastic IP를 하나 만들어서 바스티온 호스트에 접속했다.

이 구성을 다 만드는데 꽤 많은 삽질이 필요했지만 한번 만들고 나니까 변경하면서 관리하기는 꽤 쉬운 상태가 되었다. 필요하다면 이를 복사해서 다른 VPC를 만들거나 다른 리전에 VPC를 생성하는 것도 어렵지 않게 됐다. 이 VPC 구성의 전체 파일은 GitHub 저장소에 올려두었다. 이후 관리하면서 수정이 생길 수 있으므로 이 글과 완전히 일치하지 않을 수 있다.

댓글 쓰기

기술 뉴스 #82 : 17-07-15

$
0
0

웹개발 관련

  • GraphQL vs. REST : GraphQL과 REST의 유사한 점과 차이점을 비교한 글이다. 리소스 정의를 각각이 어떻게 하는지, URL 라우팅과 GraphQL의 스키마가 어떻게 다른지, Rest의 핸들러와 GraphQL의 리졸버는 어떻게 구현하고 차이점이 무엇인지를 설명하고 있어서 간략히 GraphQL의 특징을 이해하기 좋다.(영어)
  • IntersectionObserver를 이용한 이미지 동적 로딩 기능 개선 : 이미지가 많은 문서의 경우 이미지가 화면에 보일 때 동적으로 로딩하는 방식을 많이 사용하는데 제대로 구현하기가 쉽지 않은 게 사실이다. 크롬 51부터 추가된 IntersectionObserver API를 이용해서 특정 DOM이 화면에 표시되는 지 여부를 구현하는 방법을 설명하고 있다. 예제도 포함되어 있어서 화면에 보이는 요소를 얼마나 쉽게 검사할 수 있는지 잘 설명되어 있다.(한국어)
  • Getting Into HNPWA with Next.js (HNPWA 앱개발 Next.js 로 시작하기) : HNPWA는 해커뉴스를 PWA로 만들어서 예제로 공유하는 사이트인데 Next.js를 이용해서 해커뉴스 앱을 PWA로 만드는 과정부터 Ligthhouse 등으로 성능을 개선하는 부분까지 PWA 앱을 만드는 전체 과정이 잘 설명되어 있다. 전체 예시 코드도 제공되고 있으므로 PWA나 Next.js에 대한 예제 코드로 활용하기 좋다.(한국어)
  • React, Relay and GraphQL: Under the Hood of the Times Website Redesign : NY Times에서 새롭게 웹사이트를 개편하면서 다양한 언어와 서비스로 구성된 웹사이트를 GraphQL, React, Relay로 어떻게 구현했는지 설명한 글이다. 기술적으로는 간단히 GraphQL, Relay를 소개하는 느낌에 가깝고 Relay Classic에서 새로 나온 Modern으로 업그레이드하는 과정에서 나온 kyt라는 프로젝트 관리 도구도 함께 소개하고 있다.(영어)
  • Standard ECMA-262 ECMAScript® 2017 Language Specification 8th edition : ES2017(ES8)의 명세가 확정되었다.(영어)
  • MDN’s new design is in Beta : 웹 기술 관련 레퍼런스 문서로 가장 좋다고 생각하는 MDN이 새로운 문서 디자인을 베타로 적용해서 테스트하고 있다. MDN에 로그인 후 프로필에서 베타테스터로 설정하면 이용할 수 있다.(영어)

그 밖의 프로그래밍 관련

  • Diff Monster를 소개합니다 : GitHub의 Pull Request를 리뷰할 때 부족한 GitHub의 기능을 보충하기 위해서 파일 리스트를 쉽게 보고 파일별로 리뷰 완료를 확인하는 등 코드 리뷰를 편하게 할 수 있게 도와주는 웹 프로젝트.(한국어)
  • Introducing npx: an npm package runner : npm v5.2.0부터 포함된 npx를 설명한 글이다. 요즘은 npm 모듈을 글로벌로 설치하지 않고 로컬에 설치하고 사용하는게 일반적인데 이때 실행파일을 실행하려면 npm scripts에 추가해서 사용하는데 npx는 package.json를 수정하지 않고 바로 실행할 수 있다. 여기에 추가적으로 설치되어 있지 않은 npm 모듈도 실행하면 알아서 찾아서 임시로 설치해서 실행해 주므로 제너레이터 등을 사용할 때 좋다.(영어)
  • Universal Now: Now, on Every Cloud : Zeit에서 바로 서버에 배포할 수 있는 도구로 만든 now가 이제 유니버설 인터페이스를 제공해서 AWS, GCP, Azure에 배포할 수 있게 되었다. 각 클라우드의 Serverless 기능을 이용하는데 AWS의 경우 Lambda와 API Gateway를 이용한다. 서버리스 도구에서 강력한 도구가 새로 나온 느낌이다.(영어)
  • Go Reliability and Durability at Dropbox, Tammy Butow : Dropbox가 현재 golang을 어떻게 사용하고 있고 어떻게 도입했는지를 설명한 글이다. golang을 쓰기 시작하면서 많은 부분을 golang으로 바꾸어서 15개 팀 이상이 golang을 쓰고 있고 130만 라인 이상의 코드를 작성했다. 현재 아주 성공적으로 도입해서 사용하고 있고 새로운 사람이 golang에 적응할 수 있도록 스타일가이드를 알려주고 코드리뷰를 진행하면서 애플리케이션을 작성해보도록 한다고 한다.(영어)
  • Instagram Makes a Smooth Move to Python 3 : Instagramp이 10개월의 작업 끝에 Python 2에서 3으로 완전히 넘어갔는데 그 과정에 대해 인스타그램 개발자 2명과 인터뷰한 내용이다. 3~4달 동안 사용하지 않는 코드와 3가 지원하지 않는 패키지를 제거하고 2달 동안 유닛테스트를 작성한 뒤 프로덕션 코드를 4달 동안 바꿔서 현재는 완전히 Python 3으로 바꿨다고 한다. 성능에 대한 기대는 하지 않았는데 CPU, 메모리 모두 줄어드는 효과를 보았다고 한다.(영어)
  • adidas APIs : Adidas에서 RESTful API를 구축하면서 각 팀과 협의하고 가이드라인을 만들 과정을 설명한 글이다. 이 과정에서 만들어진 API 가이드라인을 공개했는데 정리가 잘 되어 있어서 API를 설계할 때 참고하기 좋다.(영어)

볼만한 링크

  • 모든 개발자들이 쉬는 시간에도 개발을 하는 것은 아니다 : 모든 개발자가 쉬는 시간이나 여가에 개발하고 트랜드를 공부하는 것은 아니라는 자신에게 맞게 시간을 보내면서 스트레스받지 말자는 자신의 얘기를 적은 글이다. 나는 쉬는 시간에 개발하는 쪽이긴 하지만 더 즐거운 일을 하자는 부분에는 동의한다. 나는 그게 더 즐거울 뿐...(한국어)
  • 데이터를 얻으려는 노오오력 : Bapul의 김영재 님이 교육 서비스에서 데이터를 억기 위해서 수년간 한 작업과 그 결과가 정리된 발표자료이다. 단순히 하나의 내부 프로젝트가 아니라 수년간 서비스 내에서 사용자에게 좋은 데이터를 수집하고 그 인사이트로 서비스를 개선하고 다시 개선하는 과정이 다 나와 있다. 데이터를 수집하기 위해서 내부 알고리즘과 UX 등을 수개월에 걸쳐서라도 개선하고 이런 데이터를 수집하고 분석하기 위해서 취해야 하는 관점 등이 나와 있는데 실제 경험을 기반으로 한 내용이라서 공감도 되고 발표 흐름이 잘 정리되어 있어서 이해하기도 좋다.(한국어)
  • 그로스 해킹 – 이보다 더 과학적일 수 없다 : Qubit이라는 마케팅 분석 플랫폼 스타트업에서 수천 개의 그로스 해킹 실험 결과를 발표한 내용을 정리 요약한 글이다. 영어고 글이 길어서 자세히 보기 어려운데 각 실험에서 어느 정도의 효과가 있는지 이 글을 통해서 알 수 있다.(한국어)

IT 업계 뉴스

프로젝트

  • bundlesize : npm 패키지로 빌드 후 특정 파일의 사이즈를 검사한 지 GitHub의 훅으로 알려주는 라이브러리. Travis CI, CircleCI에서 동작한다.
  • Babylon.js : WebGL 자바스크립트 프레임워크로 3.0부터 WebGL 2를 지원한다.
  • Uppy : 웹브라우저용 파일 업로더
  • Highlight : 키노트 등에 Syntax Highlight를 자동으로 해주는 애플리케이션.
  • Bash-Snippets : 다양한 Bash 스크립트를 모아놓은 저장소.
  • API Security Checklist : API 보안을 확인해 볼 수 있는 체크리스트로 한글로도 제공한다.

버전 업데이트

댓글 쓰기

Terraform간에 리소스 공유

$
0
0

Terraform으로 AWS 등 인프라를 관리하다 보면 하나의 Terrafrom, 조금 더 정확히는 terraform.tfstate파일에 모두 관리하지 않고 나누어서 관리하게 된다.

As Terraform configurations get larger, it's much more manageable and safer to split one large configuration into many smaller ones linked together with terraform_remote_state data sources.

Terrafrom 문서에서도 위처럼 관리하다 보면 점점 리소스가 많아지게 되는데 하나의 큰 설정으로 관리하지 않고 더 작은 설정으로 나누고 terraform_remote_state로 연결해서 사용하라고 권하고 있다.

일단 하나의 .tfstate로 관리하는 리소스가 많아지면 plan이나 apply를 할 때도 시간이 점점 오래 걸리게 되므로 자연히 여러 .tfstate로 나누게 된다. 이렇게 나누는 명확한 기준은 아직 찾지 못했지만 만들다 보면 같이 사용하는 리소스끼리 관리하면서 필요에 따라 나누게 된다. 위의 문서도 초반에 읽었었지만, 그때는 terraform_remote_state로 연결한다는 의미를 잘 이해하지 못했는데 이 글에서는 이렇게 나눈 .tfstate를 연결해서 사용하는 방법을 설명한다.

예제를 위한 VPC용 Terrafrom 구성

먼저 VPC를 하나 만들기 위해 vpc폴더 아래 Terraform을 구성해 보자. 다음과 같은 vpc/terraform.tf파일을 하나 만들어보자.(파일명을 중요하지 않다.)

terraform {
  backend "s3" {
    bucket = "kr.ne.outsider.terraform"
    key = "vpc/terraform.tfstate"
    region = "ap-northeast-1"
    encrypt = true
    lock_table = "TerraformLock"
    acl = "bucket-owner-full-control"
  }
}

이는 전에 설명한대로 .tfstate파일을 S3에 저장하는 설정이다. 여기서 kr.ne.outsider.terraform S3 버킷과 TerraformLock DynamoDB 테이블은 이미 만들어져 있다고 가정한다. 이 설정을 두고 초기화를 하면 .tfstate를 저장할 백엔드를 초기화한다.

$ terraform init
Initializing the backend...


Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your environment. If you forget, other
commands will detect it and remind you to do so if necessary.

이제 VPC를 만들어 보자. 다음은 vpc/vpc.tf파일이다.

resource "aws_vpc" "example" {
  cidr_block = "172.10.0.0/20"
  tags {
    Name = "example"
  }
}

resource "aws_subnet" "example_a" {
  vpc_id = "${aws_vpc.example.id}"
  cidr_block = "172.10.0.0/24"
  availability_zone = "ap-northeast-1a"
}

resource "aws_subnet" "example_c" {
  vpc_id = "${aws_vpc.example.id}"
  cidr_block = "172.10.1.0/24"
  availability_zone = "ap-northeast-1c"
}

이를 사용해서 terraform apply를 하면 AWS에 VPC가 만들어진다.

별도의 tfstate로 EC2 관리

위에서 만든 VPC 안에 EC2 인스턴스를 관리하기 위해 ec2폴더를 만들어서 관리하기 위해 ec2/terraform.tf를 만들어서 백엔드를 초기화한다(terraform init).

terraform {
  backend "s3" {
    bucket = "kr.ne.outsider.terraform"
    key = "ec2/terraform.tfstate"
    region = "ap-northeast-1"
    encrypt = true
    lock_table = "TerraformLock"
    acl = "bucket-owner-full-control"
  }
}

이제 EC2 인스턴스를 띄우는 Terraform 구성을 만들 차례로 다음과 같은 ec2/ec2.tf파일을 생성한다.

resource "aws_instance" "example-server" {
  ami = "ami-785c491f"
  instance_type = "t2.micro"
  subnet_id = "subnet-6315f62a"
  key_name = "key-pair-name"
  tags {
    Name = "examples"
  }
}

여기서 보면 subnet_id를 직접 하드 코딩해서 subnet-6315f62a로 지정했다. 이 서브넷은 앞에서 만든 VPC의 서브넷이기는 하지만 한 폴더에서 관리했다면 ${aws_subnet.example_a.id}처럼 참조해서 사용했겠지만 여기서는 별도의 폴더로 분리했으므로(.tfstate가 다르므로) 이렇게 참조를 할 수가 없다. AWS 웹 콘솔에 들어가서 서브넷 ID를 찾아서 지정해 줄 수는 있지만, 매번 할 때도 귀찮고 이 아이디를 보고 어떤 서브넷인지 알기가 어려우므로 코드를 읽기도 좋지 않다.

terraform_remote_state 데이터 소스

이 문제는 terraform_remote_state 데이터 소스를 사용하면 해결할 수 있다. terraform_remote_state는 원격으로 관리하는 tfstate를 데이터소스로 가져와서 참조하는 역할을 한다.

먼저 원격 상태 파일을 다른 설정에서 가져다 쓰려면 내보낼 값을 지정해 주어야 한다. 여기서는 ec2에서 vpc를 가져다 사용할 것이므로 vpc쪽에서 외부에서 참조할 수 있는 값을 지정해 주어야 한다. 다음과 같은 vpc/output.tf파일을 생성한다.

output "vpc_subnet_a_id" {
  value = "${aws_subnet.example_a.id}"
}

이는 aws_subnet.example_a.id의 값을 vpc_subnet_a_id라는 이름으로 외부에 노출하는 설정이다. 이 설정을 원격 상태 파일에 적용하려면 terraform refresh를 실행해야 한다.

$ terraform refresh
Acquiring state lock. This may take a few moments...
aws_vpc.example: Refreshing state... (ID: vpc-f7c49293)
aws_subnet.example_a: Refreshing state... (ID: subnet-6315f62a)
aws_subnet.example_c: Refreshing state... (ID: subnet-720e282a)

Outputs:

vpc_subnet_a_id = subnet-6315f62a

실행하면 마지막에 output으로 노출한 설정값이 표시된 것을 볼 수 있다.

이제 ec2에서 이 VPC의 원격 상태 파일을 참조하는 설정을 ec2/data.tf로 만든다.

data "terraform_remote_state" "vpc" {
  backend = "s3"
  config {
    bucket = "kr.ne.outsider.terraform"
    key = "vpc/terraform.tfstate"
    region = "ap-northeast-1"
    encrypt = true
    lock_table = "TerraformLock"
    acl = "bucket-owner-full-control"
  }
}

이는 데이터소스로 위에서 S3에 지정한 tfstate를 참조한 설정이다. config에는 tfstate를 정의한 설정을 같게 지정해서 사용하면 된다. 이 데이터는 원격에 있으므로 ec2폴더 쪽에서도 데이터 갱신을 한번 해주어야 한다.

$ terraform refresh
data.terraform_remote_state.vpc: Refreshing state...

이제 terraform console에서 이 값을 확인해 보면 앞에서 하드 코딩한 값이 제대로 출력되는 것을 볼 수 있다.

$ terraform console> data.terraform_remote_state.vpc.vpc_subnet_a_id
subnet-6315f62a

data.terraform_remote_state.vpc.vpc_subnet_a_id참조를 사용하면 subnet-6315f62a를 사용하는 대신 더 읽기 쉬운 이름을 지정할 수 있고 각 tfstate를 연결해서 사용할 수 있다.

댓글 쓰기

Terraform에서 Map 타입 변수의 사용

$
0
0

Terraform으로 AWS 리소스를 사용하면서 점점 변수를 많이 사용하게 된다. Terraform으로 리소스를 생성하면 그 리소스의 이름으로 참조할 수 있지만 모든 리소스를 다 Terraform으로 관리할 수 있는 것은 아니다. 경험했을 때는 사용자 이름이나 AMI 이름 등이 대표적이다.

resource "aws_instance" "demo" {
  ami = "ami-785c491f"
  instance_type = "t2.micro"
}

EC2 인스턴스를 위처럼 정의할 수 있는데 여기서 ami-785c491f는 Ubuntu Server 16.04 LTS (HVM) 이지만 이 AMI 아이디만으로는 알기가 어려우므로 이렇게 사용한다면 주석으로 어떤 AMI 인지를 적어두는 것이 차후에 관리하기가 좋을 것이다.

resource "aws_instance" "demo" {
  ami = "ami-785c491f" # Ubuntu Server 16.04 LTS (HVM)
  instance_type = "t2.micro"
}

일회성의 경우 애매하기는 하지만 가능하면 아래처럼 변수를 사용하는 것이 더 좋다고 생각한다.

variable "ubuntu_ami" {
  default = "ami-785c491f"
  description = "Ubuntu Server 16.04 LTS (HVM)"
}

resource "aws_instance" "demo" {
  ami = "${var.ubuntu_ami}"
  instance_type = "t2.micro"
}

변수를 사용하는 경우 쉽게 어떤 AMI를 사용하는지(이름을 잘 지어야겠지만) 알 수 있고 여러 곳에서 사용하는 경우 한 번에 이미지를 바꾸는 것도 가능하다.

여기서는 변수에 특정 값을 할 당했는데 Terraform의 변수는 맵을 지원하기 때문에 맵을 사용해서 여러 값을 넣는 것도 가능하다.

variable "ubuntu_ami" {
  default = {
    us-east-1 = "ami-d15a75c7"
    us-west-2 = "ami-8b92b4ee"
    ap-northeast-1 = "ami-785c491f"
    ap-northeast-2 = "ami-94d20dfa"
  }
  description = "Ubuntu Server 16.04 LTS (HVM)"
}

여러 리전을 사용하는 경우 하나의 변수를 Map으로 정의해서 리전 별로 지정하면 var.ubuntu_ami["ap-northeast-1"]와 같이 참조할 수 있고 사용할 때도 의미를 알기 쉬워서 아주 좋다.

variable "availability_zones" {
  type = "map"
  default = {
    "eu-west-1" = "eu-west-1a,eu-west-1b,eu-west-1c"
    "us-west-1" = "us-west-1b,us-west-1c"
    "us-west-2" = "us-west-2a,us-west-2b,us-west-2c"
    "us-east-1" = "us-east-1c,us-west-1d,us-west-1e"
  }
}

위처럼 Availability Zones을 리전별로 정의해 놓고 참조해서 사용하는 것도 가능하다. 맵에서 키를 찾는 lookup함수를 제공하므로 var.availability_zones["us-east-1"]대신 아래와 같이 값을 가져올 수도 있다.

> lookup(var.availability_zones, "us-east-1")
us-east-1c,us-west-1d,us-west-1e

Availability Zones는 배열로 만들어서 사용하고 싶다면 split을 사용하면 배열로 변환해서 할당할 수 있다.

> split(",", lookup(var.availability_zones, "us-east-1"))
[
  us-east-1c,
  us-west-1d,
  us-west-1e
]


댓글 쓰기

기술 뉴스 #83 : 17-08-01

$
0
0

웹개발 관련

  • ES8 was Released and here are its Main New Features : 지난달에 최종 표준이 발표된 ES2017에서 추가된 기능을 정리한 글이다. ES2017에 큰 변화는 없지만 새로 추가된 메서드나 기능들을 파악하기 좋은 글이다.(영어)
  • High Performance React: 3 New Tools to Speed Up Your Apps : React 앱에서 성능 최적화를 할 수 있는 도구를 소개하는 글이다. react_perf쿼리스트링으로 개발자 도구의 성능 툴에서 React 컴포넌트의 라이프사이클을 볼 수 있고 why-did-you-update라이브러리로 불필요하게 렌더링 되는 컴포넌트를 확인할 수 있고 React Developer Tools로 최적화는 방법을 설명하고 있다. 마지막에는 자신이 만든 상용도구에 대한 소개로 이어지지만 각 도구의 데이터를 읽는 법과 최적화 방법까지 나와 있어서 유용하다.(영어)
  • Reducing CSS bundle size 70% by cutting the class names and using scope isolation : Webpack과 babel-plugin-react-css-modules로 CSS 클래스를 a_a같은 식으로 줄여서 CSS 파일을 140KB에서 47KB까지 줄인 접근을 설명한 글이다. 보통 파일을 압축해서 제공하므로 결과적인 용량에서 아주 큰 이득은 없지만 파싱할 문서의 크기는 줄어드는 효과가 있다고 설명하고 있다.(영어)
  • SVG를 이용해 간단한 웹 게임 만들어보기 : SVG를 이용해 간단한 아케이드 게임을 만들면서 SVG를 선택한 이유와 성능 최적화를 한 과정까지 나와 있어서 웹에서 게임 등 동적인 요소를 만들 때 도움이 될 글이다.(한국어)

그 밖의 프로그래밍 관련

  • Unconventional way of learning a new programming language : 새로운 프로그래밍 언어를 배울 때 오픈소스에 기여하는 방식으로 하기를 추천하는 글이다. 실제로 글쓴이가 이 방식으로 프로그래밍 언어를 배우고 있으며 이 방식이 좋은 이유는 가이드라인을 지키면서 좋은 코드를 작성할 수 있고 해당 언어의 전문가한테 피드백을 받을 수 있는 점을 들었다. 해당 언어 저장소에서 beginnereasy-fix라벨이 붙은 이슈를 처리하는 방식을 취하고 있다.(영어)
  • GET READY: A NEW V8 IS COMING, NODE.JS PERFORMANCE IS CHANGING. : V8에 Crankshaft에서 Turborfan으로 JIT 컴파일러를 바꾸면서 Node.js 코어 팀에서 성능 개선을 위해서 알려진 JavaScript 코드 패턴으로 벤치마크를 실행한 결과이다. Crankshaft를 쓰는 V8 5.1, Crankshaft와 Turborfan을 같이 쓰는 5.8, Turbfan을 쓰는 5.9, 6.0, 6.1에서 성능이 어떻게 달라지는지를 쉽게 알 수 있다. 기존 관행이 그대로 유지되는 때도 있고 기존에 성능 향상 팁으로 사용하던 코드들이 더는 무의미한 때도 있는데 Turbofan은 아직 적용 중이지만 전체적인 성능 향상의 방향을 이해할 수 있다.(영어)
  • Say hello to HTTP/2 for Node.js Core : Node.js에 http2 모듈을 추가하는 PR에 대해서 http2 모듈을 어떻게 사용하는지 설명한 글이다. 아직 PR이 머지되지 않았고 PR이 머지되더라도 실험적 단계이지만 이 기능이 추가되면 Node.js에서 쉽게 http2를 지원할 수 있다.(영어)
  • The OAI Announces the OpenAPI Specification 3.0.0 : OAI에서 OpenAPI 명세 3.0.0을 발표했다.(영어)

볼만한 링크

  • QA != 통합테스트 : 우아한 형제의 QA 팀에서 QA 팀의 업무를 단순히 테스트로 바라보는 오해를 풀기 위해서 쓴 글로 단순 테스트가 아니라 소프트웨어 및 그 개발 과정 전체에서 품질을 높이는 노력을 하고 있다고 설명하고 있다. 그동안 개발을 하면서 잘 갖춰진 QA 팀과 일해본 적이 많지 않기 때문에 QA 팀의 지원을 잘 받으면 소프트웨어 품질에 큰 도움이 되는 걸 알고 있지만, 막상 자주 겪지 못해서 나도 은연중에 테스트 업무로만 바라보고 있었던 것을 부정하기는 어렵다.(한국어)
  • 스타트업 CTO의 일 : 8퍼센트의 CTO이신 이호성님이 그동안 CTO 역할을 하면서 CTO가 해야 하는 일을 정리한 글이다. Technical Leader, Technical Businessman, Team Manager, Product Manager의 역할별로 해야 하는 일이 잘 정리되어 있다.(한국어)
  • 이직초보 어느 개발자의 이력서 만들기 : 우아한 형제에서 일하시는 구인본님이 이력서를 개선하는 과정을 설명한 글이다. 첫 페이지의 기술 요약 및 개인 정보부터 프로젝트 요약까지 더 보기 좋은 이력서를 만들기 위한 고민 과정과 의도가 잘 나와 있어서 똑같이 만들지 않더라도 개발자 이력서가 어떤 형태로 자신을 알려야 하는지 너무 잘 설명된 글이라고 생각한다.(한국어)
  • Move Fast and Don’t Break Things: Run Development From Slack : Rod Johnson이 만든 Atomist에서 Slack을 이용해서 서버를 관리하는 방법을 설명하고 있다. GitHub에서 커밋이 올라오면 Slack으로 알림을 받을 때 단순한 알림이 아니라 CI 결과, 핑거프린트를 만들어서 변경된 모듈, 관련 이슈 등을 함께 볼 수 있도록 알려주고 여기에 버튼을 추가해서 바로 서버에 배포하거나 정보를 볼 수 있도록 강화해서 쓰고 있다.(영어)
  • Technical Leadership: Getting Started : 기술 리더쉽에 대해 설명한 글로 본인이 이 부분을 중요하게 생각하지 않고 코딩에만 열중하다가 실패한 경험을 바탕으로 쓴 글이다. 관리와 리딩을 구분해서 리딩은 사람들에게 영감을 주는 것이라고 설명하고 먼저 자기 자신을 리딩하고 다른 사람과 조직을 리딩해야 하는데 이글에서는 자신을 리딩하는 방법을 설명하고 있다.(영어)

IT 업계 뉴스

프로젝트

  • GitPoint : React Native로 만든 GitHub iOS 앱. 안드로이드 버전은 준비 중이다.
  • gpu.js : WebGL을 이용해서 GPU로 JavaScript을 실행하는 라이브러리로 사이트에서 벤치마크를 실행해보면 CPU로 돌린것 보다 상당히 빠른 속도를 보여준다.
  • TTTFI : IFTTT의 미들웨어로 IFTTT의 웹훅을 받아서 스크립트를 실행하고 다시 IFTTT로 보내준다.
  • ūsus : prerender.io처럼 SPA 앱을 크롬 디버깅 프로토콜로 미리 정적 HTML로 만들어서 제공할 수 있는 프로젝트.
  • SoftU2F : GitHub에서 만든 오픈소스로 macOS에서 U2F(Universal 2nd Factor) 하드웨어 장비 대신 소프트웨어로 인증할 수 있는 소프트웨어다.
  • chromeless : Chrome의 headless 모드를 이용해서 웹브라우저 자동화를 할 수 있게 구현한 프로젝트로 로컬 외에도 AWS Lambda에 올려서 사용할 수 있다.

버전 업데이트

댓글 쓰기


폴더별 환경 관리를 위한 direnv

$
0
0

@nacyo_t님의 소개로 direnv를 쓴지도 꽤 되었다. 이런 류의 도구의 존재는 알고 있었지만, 그동안 여러 환경을 오가야 할 필요가 많이 없었기 때문에 굳이 써야 할 필요성을 못 느끼고 있었다가 지금 업무 환경에서는 개발환경의 버전이 다른 경우도 많고 Docker를 쓰다 보니 관련 환경변수를 전역이 아닌 프로젝트별로 설정해서 써야 하는 경우가 많았다. 프로젝트 수가 많다 보니 전역으로 관리할 수 있는 양이 아니어서 사용하기 시작한 뒤로 너무 편해서 계속 쓰고 있다.

direnv

direnv는 이름 그대로 폴더별로 환경을 관리해주는 도구이다. direnv로 설정을 해 놓으면 폴더 이동을 할 때마다 자동으로 설정해놓은 환경변수나 원하는 런타임 버전 지정 등을 알아서 할 수 있다. 그래서 한번 설정해 놓으면 해당 프로젝트에서 다른 설정에 대해서는 잊어버리고 쉽게 작업을 할 수 있고 어떤 환경 설정을 해놨는지가 궁금해지면 설정 파일을 열어보면 그만이다.

direnv 설치

direnvGo로 작성되었는데 홈페이지에 나온대로 각 OS의 패키지 매니저를 이용해서 설치하거나(macOS라면 brew install direnv) 릴리스 페이지에서 OS에 맞는 바이너리를 받아서 설치해서 사용하면 된다.

설치 후에는 쉘에서 direnv가 실행되도록 해야 하므로 bash를 쓰고 있다면 ~/.bashrc파일에 eval "$(direnv hook bash)"를 추가하면 폴더 이동을 할 때마다 자동으로 실행되게 된다. bash외에 다른 쉘을 쓰고 있다면 zsh, fish, tcsh를 다 지원하므로 홈페이지를 참고해서 설정하면 된다.

.envrc

사용방법은 아주 간단한데 환경설정이 필요한 디렉터리 아래 .envrc파일을 만들면 된다. 예를 들어 project-a라는 디렉터리 아래 다음과 같이 .envrc를 만들었다고 해보자.

$ cat .envrc
export HOST=127.0.0.1
export PASSWORD=asdf

이제 project-a폴더에 진입하면 위 파일을 실행해서 자동으로 환경 변수를 설정해 주게 된다.

$ cd project-a
direnv: loading .envrc
direnv: export +HOST +PASSWORD

$ env | grep PASSWORD
PASSWORD=asdf

$ cd ..
direnv: unloading

폴더에 진입하자 direnv가 실행되고 HOST, PASSWORD환경변수를 설정하고 폴더를 빠져나오자 자동으로 다시 제거하는 것을 볼 수 있다. 이렇게 필요한 환경만 잘 설정해 놓으면 이제 작업할 때 폴더만 이동하면서 환경 걱정 없이 작업할 수 있다.

$ echo 'export USERNAME=outsider' > .envrc
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.

$ direnv allow
direnv: loading .envrc
direnv: export +USERNAME

위에서 보는 것처럼 새로운 .envrc파일이나 기존의 .envrc를 수정하는 경우에는 바로 적용되지 않고 direnv allow를 해주어야 사용할 수 있다. 이는 압축파일을 다운받거나 다른 프로젝트 파일을 받았는데 거기에 .envrc가 있어서 자동으로 실행되어 보안 문제가 발생하는 것을 막기 위함이다. 처음에는 어색했지만 한번 설정하면 자주 바꿀 일은 없으므로 크게 불편하지는 않다.

그리고 위에서 .envrc파일의 내용에서 눈치챘겠지만 direnv는 환경 변수설정 전용이 아니다. .envrc의 내용에 export USERNAME=outsider처럼 써주었는데 이는 환경변수를 설정하는 bash 명령어 그대로이다. 이 말은 쉘 명령어는 .envrc에서 사용할 수 있다는 의미라서 단순 환경변수 외에도 원하는 작업을 쉘로 작성해서 여기에 넣을 수 있다. 그래서 실제로 위키에도 언어별 환경을 설정하는 방법이 나와 있다. 프로젝트별로 Python이나 Ruby의 버전이 다른 경우에도 이 Project Layouts를 쓰면 자동으로 버전도 프로젝트에 맞게 설정해서 사용할 수 있다.

댓글 쓰기

Node.js의 v8-inpector 디버깅

$
0
0

Node.js를 디버깅하는 방법에 대해서 몇 번 글을 썼다.

Node.js도 계속 발전하면서 디버깅도 점점 좋아지고 있다. 나는 console.log로 로그를 남기면서 디버깅하는 걸 훨씬 좋아하는 편이지만 때로는 디버거를 통해서 문제를 찾아야 하는 경우도 있다. 위에서 쓴 대로 Node.js v6부터 V8의 네이티브 인스펙터가 들어오기 시작했고 이제 이 방식이 표준으로 자리 잡아서 다른 도구들도 대체되거나 인스펙터 방식으로 통일되었다.

Node.js 8.0.0부터는 v8-inspector를 완전히 지원하기 시작해서 기존에 사용하던 --debug가 폐기되고 --inspect로 통일되었고 node-inspect가 Node.js에 통합되었다. 이 글은 예제는 Node.js 8.0.0 이상에서 동작하고 지금은 8.3.0을 사용하고 있다. 자세한 내용은 Debugging Node.js Apps에 잘 나와 있다.

--inspect--inspect-brk

bin/www가 구동 파일인 간단한 Express웹 애플리케이션이 있다고 하면 이를 시작할 때 --inspect옵션을 주면 된다. --inspect-brk를 사용하면 코드 첫 줄에서 멈추므로 처음부터 디버깅해야 할 때 유용하다.

$ node --inspect bin/www
Debugger listening on ws://127.0.0.1:9229/8e704347-66ea-428a-9502-4d23ba374f9f
For help see https://nodejs.org/en/docs/inspector

위 메시지를 보면 디버거가 열려있는 것을 볼 수 있다. 이는 V8의 인터페이스이므로 이를 지원하는 다른 도구는 어떤 것이라도 사용할 수 있지만 여기서는 크롬 개발자도구를 사용해 보자.

크롬 브라우저에서 <chrome://inspect/>에 접속하면 Remote Target에서 방금 실행한 Express 애플리케이션을 볼 수 있다.

크롬의 chrome://inspect

디버깅할 타겟의 "inspect" 버튼을 클릭하면 크롬 개발자도구가 열리고 웹페이지에서 JavaScript를 디버깅하듯이 개발자 도구로 디버깅을 할 수 있다.

크롬 개발자도구에서 express 앱을 디버깅하는 화면

$ node --inspect bin/www
Debugger listening on ws://127.0.0.1:9229/8e704347-66ea-428a-9502-4d23ba374f9f
For help see https://nodejs.org/en/docs/inspector
Debugger attached.

개발자 도구를 연결하면 터미널에서 Debugger attached.라고 나온 것을 볼 수 있다.

기본 포트는 9229인데 다른 포트를 사용하고 싶다면 --inspect=port--inspect-brk=port를 사용하면 된다.

node-inspect

node-inspect는 Node.js에 내장된 디버거로 --inpect로 연 디버깅 포트에 접속해서 디버깅할 수 있는 디버거이다. 디버거는 node inspect [js 파일]로 시작한다.(--inpect대신 inspect이다.)

$ node inspect bin/www< Debugger listening on ws://127.0.0.1:9229/a95f8658-9536-40ee-8a09-57c9313f4947< For help see https://nodejs.org/en/docs/inspector< Debugger attached.
Break on start in bin/www:1> 1 (function (exports, require, module, __filename, __dirname) {
  2
  3 /**
debug>

커맨드 라인에서 디버깅을 할 수 있는데 다음과 같은 명령어를 사용할 수 있다.

  • cont, c - 계속 실행한다.
  • next, n - 다음 단계로 이동
  • step, s - 단계 안으로 이동
  • out, o - 단계 밖으로 이동
Break on start in bin/www:1> 1 (function (exports, require, module, __filename, __dirname) {
  2
  3 /**
debug> next
break in bin/www:7
  5  */
  6> 7 var app = require('../app');
  8 var debug = require('debug')('express-test:server');
  9 var http = require('http');
debug> step
break in internal/module.js:10
  8   function require(path) {
  9     try {>10       exports.requireDepth += 1;
 11       return mod.require(path);
 12     } finally {
debug> out
break in app.js:28
 26 app.use('/users', users);
 27>28 debugger;
 29
 30 // catch 404 and forward to error handler
debug>

위처럼 next를 입력하면 다음 단계로 넘어가고 step, out으로 함수 안에 들어가거나 나올 수 있다. debugger;를 코드에 넣어두면 브레이크 포인트로 쓸 수 있으므로 cont를 입력하면 debugger;를 만날 때까지 실행하고 멈춘다.

run이나 restart를 입력하면 처음부터 다시 실행할 수 있고 repl을 입력하면 현 위치에서 코드를 실행해 보거나 원하는 변수를 출력해 볼 수 있다.

debug> cont
break in app.js:28
 26 app.use('/users', users);
 27>28 debugger;
 29
 30 // catch 404 and forward to error handler
debug> repl
Press Ctrl + C to leave debug repl> console.log(app.settings)< { 'x-powered-by': true,<   etag: 'weak',<   'etag fn': [Function: wetag],<   env: 'development',<   'query parser': 'extended',<   'query parser fn': [Function: parseExtendedQueryString],<   'subdomain offset': 2,<   'trust proxy': false,<   'trust proxy fn': [Function: trustNone],<   view: [Function: View],<   views: '/Users/outsider/Dropbox/projects/temp/express-test/views',<   'jsonp callback name': 'callback',<   'view engine': 'jade' }> undefined>

좀 더 자세한 내용은 Debugger 문서를 참고하면 된다.

댓글 쓰기

기술 뉴스 #84 : 17-08-15

$
0
0

웹개발 관련

  • Top Resources to Explore React Fiber : React v16부터 도입되는 Fiber를 설명한 자료를 정리한 글이다. 처음 Fiber를 소개한 자료부터 Fiber가 어떤 의미가 있고 Fiber의 구조와 동작을 잘 설명한 자료를 링크 걸고 각 글이 어떤 설명을 하고 있는지 잘 정리되어 있다.(영어)
  • Learning React.js is easier than you think — Interactive Examples : React 엘리먼트를 만드는 과정을 기본적인 부분부터 props와 state의 사용 및 생명주기까지 설명한 글이다. 각 과정에서 글 중간에 직접 실행해 볼 수 있도록 코드 스니팻과 결과가 나와 있어서 직접 사용해보면서 설명을 이해하기에 좋다.(영어)
  • All About React Router 4 : 버전 3에서 완전히 새로 작성된 React Route 4를 설명한 글이다. 사용하는 방법부터 완전히 달라졌기 때문에 3에 익숙했던 글쓴이가 4를 어떤 관점으로 접근해야 하는지 기본적인 라우팅과 레이아웃을 섞는 예제부터 중첩된 레이아웃, URL을 매칭시키는 방법까지 4에 대해서 자세히 설명한 글이다.(영어)
  • React Animations in Depth : React에서 애니메이션을 사용할 수 있는 방법을 비교 정리한 글이다. CSS, JS로 구현하는 방법부터 React Motion, Animated, Velocity-React 라이브러리의 장단점 및 사용 예제까지 제공해서 참고하기 좋다. 이 글은 React Native까지 고려하고 있어서 해당 방법이 React Native에서 사용할 수 있는지도 정리되어 있으므로 해당 부분까지 고려한다면 더욱 유용하다.(영어)
  • Bootstrap 4 Beta : 2년 만에 CSS 프레임워크의 대표 격인 Bootstrap 4의 베타버전이 나왔다.(영어)

그 밖의 프로그래밍 관련

  • AWS 비용 얼마까지 줄여봤니? : AWS 비용을 줄이려고 개발 서버로 쓰는 EC2 같은 경우는 업무시간에만 사용하면 되므로 DynamoDB, Lambda로 스케쥴러를 구현해서 의존관계에 따라 정해진 시간에만 EC2 인스턴스를 키도록 설정한 내용이다.(한국어)
  • AWS Auto Scalinging Group 을 이용한 배포: 레진에서 AWS의 오토 스케일링 그룹을 2개 만들어서 블루/그린 배포 전략을 이용하는 방법을 소개한 글이다. 오토 스케일링 그룹을 2개 만든 이유와 어떻게 처리해서 배포하고 롤백하는지까지 나와 있다.(한국어)
  • Up — Deploy serverless apps in seconds : 전에 gh-polls을 공개해서 Up이라는 프로젝트의 존재에 관해서 얘기했던 TJ Holowaychuk가 Up을 공개하고 그 사용방법을 공개했다. Apex보다 간단하게 AWS Lambda와 API Gateway를 이용해서 웹 애플리케이션을 올릴 수 있게 하는 간단한 도구로 AWS 인프라를 추상화해서 아주 간단하게 사용할 수 있다.(영어)
  • ES Modules in Node Today! : 현재 Node.js는 CommonJS의 모듈을 사용하고 있는데 이제는 ECMAScript Modules가 생겼으므로 이 둘을 모두 지원하는 데 어려움을 가지고 있다. .mjs방식으로 가고 있는 것 같지만 정식 도입되려면 시간이 더 필요한데 @std/esm는 현재 Node 프로젝트에서 바로 설치해서 CommonJS와 ES Modules를 같이 사용할 수 있게 하는 방식이다. ES Modules는 import방식을 사용하고 간단한 변환을 통해서 사용할 수 있게 하는데 이 방식이 현재 왜 좋은 방법인지까지 설명되어 있다.(영어)
  • Node.js Errors — Changes you need to know about : Node.js 8.x.x 부터 오류를 비교하기 쉽도록 오류 code가 들어가게 된 배경과 이를 어떻게 사용할지를 설명한 글이다.(영어)
  • Elixir School 한국어판 : Elixir 프로그래밍 언어를 배울 수 있는 Elixir School의 한국아 버전이다.(한국어)

볼만한 링크

  • 리디에서 CTO가 하는 일 : Ridi의 CTO가 자기 일에 대해서 사내 발표한 발표자료다. 리디북스의 서비스를 좋아하는 편인데 사내에서 개발자 조직을 어떻게 7년 동안 키워왔는지를 볼 수 있는 재미난 자료다.(한국어)
  • 우리은행 + Eversafe == NULL : 우리 은행에 적용된 Eversafe라는 보안 앱이 절대 뚫리지 않는다는 기사를 보고 글쓴이가 Eversafe를 리버스 엔지니어링 해서 내부 구조를 분석한 글이다. 회사가 언급되어 논란에 관한 얘기가 뒷부분에 나오지만 보안 쪽은 자세히 알지 못해서 해당 앱을 분석해서 구조를 찾아가는 과정만으로도 아주 재미있었다.(한국어)

IT 업계 뉴스

프로젝트

  • share-this : Medium처럼 텍스트를 선택하면 툴팁으로 선택한 텍스트를 공유할 수 있게 하는 JavaScript 라이브러리.
  • send : Mozilla에서 만든 실험적 프로젝트 일정 시간 동안만 유효한 링크로 파일을 공유할 수 있게 하는 서비스다.
  • Ring UI : JetBrains에서 웹 프로젝트에 사용하는 React UI 컴포넌트를 오픈소스로 공개했다.
  • Hazel : Electron 앱의 업데이트 서버.
  • s2client-api : Blizzard에서 공개한 스타크래프트 2 API 클라이언트.
  • Kompose : Kubernetes에서 공개한 프로젝트로 Docker Compose파일을 Kubernetes에서 띄울 수 있는 도구.
  • TimescaleDB : PostgreSQL에서 타임시리즈 데이터를 사용할 수 있게 하는 확장.

버전 업데이트

댓글 쓰기

Headless Chrome으로 AWS Lambda에서 웹사이트 스크린샷 찍기 #1

$
0
0

이전에 Headless Chrome의 사용방법에 관한 글을 올리고 Chrome의 headless_shell을 직접 컴파일해서 사용하는 방법에 대해서도 올렸다.(이 글이 6월이었다니...)

이전 글에서도 얘기했듯이 headless_shell을 테스트해보는 이유는 AWS Lambda에서 사용해보기 위함이다. AWS Lambda에서 사용할 수 있으면 다른 환경에서는 훨씬 쉽게 사용할 수 있고 Headless Chrome 같은 경우 자동화에 활용할 가능성이 크므로 Lambda 같은데 올려서 쓸 수 있다면 제격이라고 생각했다. Chrome 버전이 계속 올라가긴 하겠지만 현재 Headless Chrome으로 어디까지 할 수 있고 어떻게 활용할 수 있는지 파악해 놓고자 함이었다.

웹페이지 스크린샷

Internet Archive Wayback Machine처럼 내가 원하는 사이트를 정기적으로 계속 스크린샷을 찍으려고 한다. 요즘은 잘 안 바꾸지만 내 블로그의 디자인을 변경하는 때도 있으므로 예전부터 로컬에서 스크린샷을 찍어서 보관하고 있었다. 로컬에서 돌리는 건 아무래도 환경마다 달라서 귀찮으므로 이를 Labmda에 올려서 알아서 돌아가게 하면 좋겠다는 생각을 했다. 뭐 이전에도 이런 작업은 충분히 가능했지만 귀찮아서 놔두다가 Headless Chrome을 사용해 보기 좋아서 선택했다.

물론 이는 사이트에 접속해서 스크린샷만 찍는 것이므로 브라우저의 동작까지 제어해야 하는 자동화보다는 훨씬 간단한 작업이지만 Lambda에서 Headless Chrome을 돌려보기에는 적당한 주제라고 생각했다.(기대보다 훨씬 험난했지만...)

이렇게 작성한 Lambda 함수는 GitHub에 올려두었지만이 글 이후에도 소스를 계속 수정할 것 같으므로 여기서는 만들면서 겪은 이슈 위주로 정리해 보려고 한다. Lambda를 쓸 때 apex를 사용하고 있지만, 이 글에서는 설명이 복잡하므로 따로 만들어서 직접 Lambda에 업로드 하려고 한다.

스크린샷 Lambda

AWS Lambda에 올릴 Node.js 코드를 예시로 작성할 예정이고 환경은 Node.js 6.10을 사용할 것이다.

Chrome Launcher

크롬을 사용할 것이므로 일단 크롬을 실행해야 하는데 나는 chrome-launcher를 선택했다. 쉘 명령어로 직접 headless_shell을 실행해도 되지만 이경우 관리가 좀 귀찮을 것으로 생각했고 구글 크롬에서 관리하는 Lighthouse 아래 있는 chrome-launcher가 사용하기 적당해 보였다.

관련해서 몇 가지 참고할 부분이 있는데

  • 현재 0.6.0 인데 0.4.0 이전까지는 Lighthouse와 같이 배포되었으므로 require('lighthouse/lighthouse-cli/chrome-launcher')처럼 가져와야 했지만, 이제는 별도의 npm 모듈로 배포되므로 바로 require('chrome-launcher')로 가져오면 된다. 0.4.0 이전 버전을 사용하는 글을 참고한다면 참고해야 한다.
  • 구글 크롬팀에서 chrome-launcher외에 Headless Chrome을 Node.js로 제어할 수 있는 Puppeteer을 얼마 전에 공개했다. 이번에 만든 Lambda도 Puppeteer으로 변경할 예정이긴 하지만 아직 사용해보지 않았다. 새로 시작한다면 Puppeteer으로 바로 시작하는 게 더 좋을 수도 있다.

현재 chrome-launcher버전은 0.6.0이다. 이제 Headless Chrome을 사용할 코드를 작성하기 위해서 chrome.js을 다음과 같이 만든다. 경험상 Lambda를 쓸 때 위의 index.js는 아주 간단하게만 둔 채로 다른 로직은 별도의 파일로 작성하는 게 테스트도 작성하기 쉽고 훨씬 낫다. 먼저 chrome-launcher를 설치하고(npm install chrome-launcher) chrome.js에 다음과 같은 코드를 작성한다. 현재 버전은 0.6.0이다.

// chrome.js
const chromeLauncher = require('chrome-launcher');

const launchChrome = () => {
  return chromeLauncher.launch({
    chromeFlags: [
      '--headless',
      '--disable-gpu',
      '--no-sandbox',
      '--vmodule',
      '--single-process',
    ],
    logLevel: 'verbose',
  })
  .then((chrome) => {
    console.log(`Headless Chrome launched with debugging port ${chrome.port}`);
    return chrome;
  });
};

launchChrome();

간단히 chromeLauncher.launch()로 원하는 플래그와 옵션을 지정해서 크롬을 실행하는 코드이다.

  • chromeFlags은 크롬에 직접 전달하는 플래그이다. chromeLauncher에서 지원하지 않는 옵션은 이렇게 지정해야 한다.

    • --headless은 Headless 모드로 띄우기 위해 실행했다.
    • --disable-gpu는 GPU의 사용을 끈 것이다. 문서에 따르면 Mesa 라이브러리가 없는 경우 오류를 피하려면 필요하다는데 내 테스트에서는 없어도 큰 문제는 없었다. 일단 문서에 그렇게 나와 있으니 지정했다.
    • --no-sandbox는 macOS에서는 괜찮았지만, Linux 환경(특히 Lambda)에 가면 이 옵션이 없으면 No usable sandbox!라는 오류가 발생하고 문서를 참고하라고 나온다.
    • --vmodule--single-process는 좀 묘한 옵션인데 macOS와 로컬에서 Linux 환경에서도 다 괜찮았는데 Lambda에만 올리면 Headless Chrome을 정상적으로 띄웠음에도 chrome devtools protocol에 접속하지 못하는 문제가 생긴다. 좀 더 정확히는 크롬을 띄우고 chrome devtools protocol로 리스트를 요청하면 chrome devtools protocol에 접속 가능한 URL과 웹소켓 주소 등이 나와야 하는데 아무것도 주지 않는다. 크롬 버그인지 어떤지까지는 추적을 못 했지만 이 두 옵션을 지정하면 Lambda에서도 정상 동작한다.
  • logLevel은 런처의 로그 메시지 설정이다. 문제가 생길 때마다 디버깅을 해야 해서 verbose로 아예 켜두었다.

위 코드를 실행해보자.

$ node chrome.js
  ChromeLauncher:verbose created /var/folders/k8/j1j0fjkn5_vdzxybfqq0r7lc0000gn/T/lighthouse.XXXXXXX.8tqw7dbE +0ms
  ChromeLauncher:verbose Launching with command:
  ChromeLauncher:verbose "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" --disable-translate --disable-extensions --disable-background-networking --safebrowsing-disable-auto-update --disable-sync --metrics-recording-only --disable-default-apps --mute-audio --no-first-run --remote-debugging-port=50069 --user-data-dir=/var/folders/k8/j1j0fjkn5_vdzxybfqq0r7lc0000gn/T/lighthouse.XXXXXXX.8tqw7dbE --headless --disable-gpu --no-sandbox --homedir=/tmp --data-path=/tmp/data-path --disk-cache-dir=/tmp/cache-dir --vmodule --single-process about:blank +588ms
  ChromeLauncher:verbose Chrome running with pid 7834 on port 50069. +4ms
  ChromeLauncher Waiting for browser. +1ms
  ChromeLauncher Waiting for browser... +0ms
  ChromeLauncher Waiting for browser..... +511ms
  ChromeLauncher Waiting for browser.....✓ +3ms
Headless Chrome launched with debugging port 50069

로그를 켜두었으므로 실행과정을 볼 수 있는데 정상적으로 Headless Chrome이 뜬 것을 볼 수 있다. --remote-debugging-port의 기본 포트는 9222인데 여기서는 지정하지 않았으므로 동적으로 할당해서 사용한다. 크롬이 실행되면서 반환한 객체는 { pid: 5891, port: 62993, kill: [Function: kill] }처럼 생겼다. 프로세스의 pid와 디버깅 포트 정보가 있고 실행한 크롬을 죽일 수 있는 kill()함수가 있다.

headless_shell

chrome-launcher는 설치된 크롬을 자동으로 찾는다. Chrome Canary가 설치되어 있다면 이를 먼저 사용하고 아니면 Chrome을 찾는다.

사용 중인 데스크톱에는 보통 크롬이 설치되어 있겠지만 서버의 경우는 다르다. 나 같은 경우도 서버, 여기서는 AWS Lambda를 사용하기 위해서 headless_shell만 별도로 컴파일했다. 이렇게 별도로 있는 크롬 바이너리를 실행하려면 CHROME_PATH로 환경 변수로 크롬의 위치를 지정해 주어야 한다.

AWS Lambda라면 코드를 업로드 했을 때 /var/task에 올려지므로 업로드하는 코드에 headless_shell을 포함한다면 CHROME_PATH=/var/task/headless_shell로 환경변수를 지정해야 Lambda 내에서도 chrome-launcher가 정상적으로 크롬을 찾아낸다. 물론 환경변수보다 OS 설치된 크롬을 먼저 찾는다.

참고삼아 얘기하지만, chrome-launcher 0.4.0 이전 버전에서는 LIGHTHOUSE_CHROMIUM_PATH환경변수를 사용했는데 지금은 CHROME_PATH로 바뀌었다.

Chrome Devtools Protocol

크롬에는 Chrome DevTools Protocol이 있다. 이는 크롬에서 개발자도구로 웹사이트를 디버깅, 프로파일링, 검사하듯이 원격에서 이 프로토콜을 사용하면 크롬으로 똑같은 일을 할 수 있다. 많은 도구가 이 프로토콜을 이용하고 있다.

이 프로토콜의 동작을 확인해보자.

macOS에서 bash를 쓴다면 설치된 크롬 카나리를 터미널에서 실행할 수 있도록 다음과 같이 별칭을 준다.

$ alias chrome-canary="/Applications/Google\ Chrome\ Canary.app/Contents/MacOS/Google\ Chrome\ Canary"
$ chrome-canary \
  --headless --disable-gpu \
  --remote-debugging-port=9222

[0824/223949.914930:WARNING:dns_config_service_posix.cc(156)] dns_config has unhandled options!

DevTools listening on ws://127.0.0.1:9222/devtools/browser/a2c2fe73-a8bc-49c8-84aa-61a251a7def9

헤드리스 모드로 크롬 카나리를 실행하고 원격 디버깅 포트를 9222로 실행하자 개발자 도구가 9222 포트를 리스닝하고 있는 것을 볼 수 있다. 이렇게 실행해 놓은 채로 터미널을 하나 더 열어서 이 디버깅 포트에 요청을 보내보자.

$ curl http://localhost:9222/json/version
{
   "Browser": "HeadlessChrome/62.0.3194.0",
   "Protocol-Version": "1.2",
   "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3194.0 Safari/537.36",
   "V8-Version": "6.2.333",
   "WebKit-Version": "537.36 (@c14400a94cb4f43e0e9b02e59e93cf5a082c6408)",
   "webSocketDebuggerUrl": "ws://localhost:9222/devtools/browser/a2c2fe73-a8bc-49c8-84aa-61a251a7def9"
}

$ curl http://localhost:9222/json/list
[ {
   "description": "",
   "devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/e873e5cb-f1db-4892-87cf-5296fbc7485c",
   "id": "e873e5cb-f1db-4892-87cf-5296fbc7485c",
   "title": "about:blank",
   "type": "page",
   "url": "about:blank",
   "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/e873e5cb-f1db-4892-87cf-5296fbc7485c"
} ]

/json/version으로 요청을 보내면 현재 크롬의 정보를 볼 수 있고 /json/list를 이용하면 접속할 수 있는 Chrome Devtools Protocol 목록을 반환한다. 목록이 배열인 이유는 크롬에서 탭을 여러 개 열 수도 있기 때문이다. 각 클라이언트는 이 정보 등을 이용해서 Chrome Devtools Protocol을 구현하고 프로토콜에서 제공하는 객체로 크롬을 제어할 수 있다. DOM, Page, Network 등 세세하게 다룰 수 있게 모두 제공하는데 직접 클라이언트를 개발하는 게 아니라면 세부 내용을 다 알 필요는 없어 보인다. 필요할 때 위 문서를 참고해보는 정도로 충분하다.

chrome-remote-interface

Chrome DevTools Protocol을 직접 사용하는 것은 어려우므로 이미 존재하는 많은 라이브러리가 존재하는데 공식 문서에서 권장하는 chrome-remote-interface를 사용했다.

chrome-remote-interface를 설치하고(npm install chrome-remote-interface) 앞에서 작성한 launchChrome()를 이용해서 Chrome Devtools Protocol에 연결해보자. 현재 버전은 0.24.4다.

// chrome.js
const CDP = require('chrome-remote-interface');

// launchChrome 부분 생략

let launchedChrome = null;

const connectChrome = () => {
  return launchChrome()
    .then((chrome) => {
      launchedChrome = chrome;
      return CDP.Version({ port: launchedChrome.port });
    })
    .then(info => console.log('version: ', info))
    .then(() => CDP({ port: launchedChrome.port }))
    .then((client) => {
      console.log('client connected');
    });
};

connectChrome();

이를 실행하면 다음과 같이 실행된 크롬에 접속해서 정보를 가져오고(CDP.Version) 연결을 하는 것을 볼 수 잇다. 여기서 CDP({ port: launchedChrome.port }))에서 포트만 지정했는데 host를 지정하지 않으면 localhost를 사용한다.

node chrome.js
  ChromeLauncher:verbose created /var/folders/k8/j1j0fjkn5_vdzxybfqq0r7lc0000gn/T/lighthouse.XXXXXXX.rNE9ehBV +0ms
  ChromeLauncher:verbose Launching with command:
  ChromeLauncher:verbose "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" --disable-translate --disable-extensions --disable-background-networking --safebrowsing-disable-auto-update --disable-sync --metrics-recording-only --disable-default-apps --mute-audio --no-first-run --remote-debugging-port=64165 --user-data-dir=/var/folders/k8/j1j0fjkn5_vdzxybfqq0r7lc0000gn/T/lighthouse.XXXXXXX.rNE9ehBV --headless --disable-gpu --no-sandbox --homedir=/tmp --data-path=/tmp/data-path --disk-cache-dir=/tmp/cache-dir --vmodule --single-process about:blank +528ms
  ChromeLauncher:verbose Chrome running with pid 29094 on port 64165. +16ms
  ChromeLauncher Waiting for browser. +0ms
  ChromeLauncher Waiting for browser... +0ms
  ChromeLauncher Waiting for browser..... +512ms
  ChromeLauncher Waiting for browser.....✓ +2ms
Headless Chrome launched with debugging port 64165
version:  { Browser: 'HeadlessChrome/62.0.3196.0',
  'Protocol-Version': '1.2',
  'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/62.0.3196.0 Safari/537.36',
  'V8-Version': '6.2.363',
  'WebKit-Version': '537.36 (@1bdf82f83c51a1f42672bc8e59494d238269fc6f)',
  webSocketDebuggerUrl: 'ws://localhost:64165/devtools/browser/87f5f38e-cae0-43c2-b595-d199d2c73d86' }
client connected

여기서 CDP({ port: launchedChrome.port }))가 반환한 client객체는 Chrome DevTools Protocol에 있는 Page, Memory, Inspector, CSS, CacheStorage같은 객체가 있으므로 이를 이용해서 크롬을 조작하면 된다.

웹사이트 스크린샷 찍기

이제 준비가 다 되었으니 chrome-remote-interface를 이용해서 스크린샷을 찍을 차례다.

// chrome.js
const chromeLauncher = require('chrome-launcher');
const CDP = require('chrome-remote-interface');

// launchChrome 부분 생략

const connectChrome = () => {
  return launchChrome()
    .then((chrome) => {
      launchedChrome = chrome;
      return CDP.Version({ port: launchedChrome.port });
    })
    .then(info => console.log('version: ', info))
    .then(() => CDP({ port: launchedChrome.port }))
    .then((client) => {
      console.log('client connected');

      const { DOM, Emulation, Network, Page } = client;

      return Page.enable()
        .then(DOM.enable)
        .then(Network.enable)
        .then(() => {
          const deviceMetrics = {
            width: 1280,
            height: 1000,
            deviceScaleFactor: 0,
            mobile: false,
            fitWindow: false,
          };
          return Emulation.setDeviceMetricsOverride(deviceMetrics);
        })
        .then(() => Page.navigate({ url: 'https://github.com' }))
        .then(Page.loadEventFired)
        .then(() => Emulation.setVisibleSize({ width: 1280, height: 1000 }))
        .then(Page.captureScreenshot)
        .then(screenshot => new Buffer(screenshot.data, 'base64'))
        .catch((err) => {
          client.close();
          throw err;
        });
    })
    .catch((err) => {
      console.log(err);
      launchedChrome.kill();
      throw err;
    });
};

module.exports = {
  connectChrome,
};

위 코드가 CDP로 웹페이지를 띄워서 페이지 로딩이 끝나면 스크린샷을 찍어서 Buffer를 반환하는 코드이다. 간단히 설명하자면...

  • const { DOM, Emulation, Network, Page } = client;는 클라이언트에서 사용할 객체만 가져왔다.
  • Page.enable()에서 페이지를 활성화한다.
  • DOM.enable에서 해당 페이지에 DOM을 활성화한다.
  • Network.enable에서 해당 페이지의 네트워크 활동을 추적할 수 있게 한다.
  • Emulation.setDeviceMetricsOverride에서 디바이스의 스크린 설정을 지정한다. 여기서는 width, height를 지정하고(뒤에서 재지정한다.) 모바일 여부 등을 설정할 수 있는데 여기서는 특별한 설정은 하지 않았다.
  • Page.navigate에서 지정한 URL로 페이지를 이동한다.
  • Page.loadEventFired는 웹페이지에서 load이벤트가 발생하면 실행된다.
  • Emulation.setVisibleSize에서는 스크린샷을 찍기 위해서 페이지의 viewport 크기를 지정한다. 문서를 보면 deprecated로 표시되고 있는데 chrome-remote-interface에서 아직 이 방법으로 안내하고 있어서 그대로 사용했다. 아직 다른 방법은 찾지 못했다.
  • Page.captureScreenshot에서는 현재 페이지의 스크린샷을 찍는다.
  • screenshot => new Buffer(screenshot.data, 'base64')에서는 Page.captureScreenshot가 반환한 base64데이터를 Buffer로 생성해서 반환한다.

참고로 스크린샷을 찍는 작업을 하면서 여러 웹사이트를 한 번에 찍으려고 할 때 매번 크롬을 띄우고 DevTools Protocol에 붙을 필요가 없으므로 한번 연결해 놓고 URL만 변경하면서 계속 스크린샷을 찍으려고 할 때 Emulation.setVisibleSizePage.loadEventFired이후에 하지 않으니까 처음 스크린샷만 지정한 사이즈로 찍히고 이후부터는 기본값인 800x600으로 찍히는 문제가 있었다. 이 문제가 Chrome의 버그인지 chrome-remote-interface의 버그인지는 아직 잘 모르겠다.

파일을 S3에 업로드하기

이 코드를 Lambda에서 실행할 것이므로 파일을 로컬에 저장할 수 없으므로 다른 스토리지에 저장해야 한다. 여기서는 S3에 저장하려고 아래와 같은 s3.js를 만들었다.

// s3.js
const AWS = require('aws-sdk');

const s3 = new AWS.S3({ apiVersion: '2006-03-01' });

module.exports = {
  uploadFile: (buffer) => {
    return new Promise((resolve, reject) => {
      const params = {
        Bucket: 'outsider-demo',
        Key: 'screenshot.png',
        Body: buffer,
      };
      return s3.putObject(params, (err) => {
        if (err) { return reject(err); }
        return resolve();
      });
    });
  },
};

aws-sdk를 이용해서 전달받은 Buffer를 S3 버킷(여기서는 outsider-demo)에 저장하는 Promise 코드이다. 예시를 간단하게 하려고 파일명 등은 하드 코딩했다.



이 글은 Headless Chrome으로 AWS Lambda에서 웹사이트 스크린샷 찍기 #2로 이어진다.

댓글 쓰기

Headless Chrome으로 AWS Lambda에서 웹사이트 스크린샷 찍기 #2

$
0
0

이 글은 Headless Chrome으로 AWS Lambda에서 웹사이트 스크린샷 찍기 #1에서 이어진 글이다.



AWS Lambda 함수 작성

AWS Lambda에서는 시작 파일이 필요하므로 index.js를 만들어야 한다.

// index.js
exports.handler = (event, context, callback) => {
  context.succeed('Hello World');
};

Lambda에서 exports.handler를 실행하므로 이 함수가 시작점이 된다. 앞에서 작성한 모듈을 사용해서 스크린샷을 찍어서 S3에 저장하도록 코드를 수정했다.

// index.js
const { connectChrome } = require('./chrome');
const { uploadFile } = require('./s3');

exports.handler = (event, context, callback) => {
  connectChrome()
    .then(uploadFile)
    .then(context.succeed)
    .catch(context.fail);
};

이제 Lambda 함수를 다 작성했다. 로컬에서 실행할 때는 데스크톱에 설치된 크롬을 사용하지만, Lambda 환경에는 Chrome이 없으므로 headless_shell을 포함해야 한다. 크롬에서 빌드한 headless_shellGitHub에 올려두었으므로이 파일을 다운받아서 저장한 뒤 같이 업로드하면 된다.

├── chrome.js
├── headless_shell
├── index.js
├── node_modules
└── s3.js

그래서 파일 구조를 보면 위와 같은 형태가 된다.

$ zip -r function.zip .

Lambda에 올리려면 함수를 zip으로 압축해야 하므로 현재 폴더를 function.zip으로 압축했다. headless_shell이 130MB인데 function.zip로 압축하면 44MB가 된다.

여기서 Lambda에 올릴 수 있는 압축파일의 최대 크기는 50MB이므로(실제로는 좀 더 올라가지만...) 44MB는 문제없이 headless_shell를 사용할 수 있다. 다만 여기서는 예제코드라서 간단하지만 npm 모듈을 더 추가하다 보면 금방 50MB를 넘어설 수 있다. 예제는 최대한 간단하게 작성하지만, 실제 내가 Lambda를 쓸 때는 Apex를 사용하므로 배포할 때 hook을 지정해서 webpack으로 필요한 코드만 배포되도록 사용하고 있다. 이렇게 하면 50MB가 넘어갈 일은 없어 보인다.

AWS Lambda 함수 배포

배포 준비도 끝났으니 AWS Lambda에 올려서 실행해 보자.

AWS Lambda 생성

AWS Lambda에서 새로운 함수를 만들어서 zip 파일로 앞에서 만든 function.zip을 업로드한다. 이때 Lambda 내에서 headless_shell을 사용할 수 있도록 앞에서 설명한 대로 CHROME_PATH환경변수로 /var/task/headless_shell로 지정한다. 업로드한 코드는 모두 /var/task밑으로 올라간다. 메모리 설정은 256MB로 지정하고 타임아웃은 60초로 지정했다. 예시에서는 스크린샷을 1장만 찍지만 여러 장을 찍는다면 사용 메모리는 점점 올라가지만 1장만 찍는다고 하더라도 128MB로는 부족하다.

업로드한 Lambda 함수 실행 결과

AWS Lambda에서 제공하는 테스트 기능으로 Lambda를 실행한 결과이다. 성공적으로 실행이 되었고 11초가 걸렸고 153MB의 메모리를 사용했다. 여기서는 Lambda를 실행하는 IAM Role과 S3 버킷에 대한 접근 권한에 대한 설명은 생략했다. S3 버킷에 파일을 제대로 올릴 수 있도록 권한을 주어야 한다.

Lambda에서 생성한 깃헙 스크린샷

위 파일은 Lambda로 찍은 GiHub.com의 스크린샷이다. 지정한 대로 정상적으로 찍은 것을 알 수 있고 위 예시를 수정하면 Lambda에서 원하는 웹사이트의 스크린샷을 자동으로 찍어서 저장하게 할 수 있다.

CJK 폰트 문제

여기까지 테스트를 했을 때 이제 다 작성한 줄 알았지만 실제로 사용하려고 하니까 그렇지 않았다.

이 블로그의 스크린샷에서 한글이 깨진 화면

위는 Lambda로 이 블로그의 스크린샷을 찍은 화면인데 보다시피 한글이 제대로 나오지 않는다. 브라우저는 OS에 설치된 폰트를 사용하는데 Lambda에는 한글 폰트가 설치되어 있지 않으므로 당연히 한글이 제대로 출력되지 않는다. 내 블로그는 한글이 문제가 된 것이지만 폰트 문제에서 보통 그렇듯이 중국어, 일본어에서도 당연히 같은 문제가 발생할 것이다.

결국, Lambda에서 실행하는 headless Chrome이 CJK 폰트를 사용할 수 있게 설정해 주어야 한다. 이런 문제는 다른 headless chrome 프로젝트에서도 이슈가 되고 있는데 일단 나는 Google Noto 폰트를 사용하기로 했다.

Noto CJK에서 CJK 폰트를 제공하므로 이 중에서 한글 폰트인 NotoSansKR-[weight].otf를 다운로드 받았다. 이 안에는 많은 weight가 포함되어 있는데 NotoSansKR-Regular.otf를 여기서 사용한다. 다른 Weight나 CJK 폰트가 다 필요하다면 다른 파일을 사용해야 한다.

구글의 폰트 관련 문서를 보면 리눅스에서는 HOME디렉터리 아래 .fonts디렉터리를 만들고 그 아래 폰트 파일을 두면 크롬이 자동으로 폰트를 로딩해서 사용한다. Lambda에서도 이 방법을 사용해야 하는데 업로드하는 파일에 넣으면 /var/task아래 만들고 여기를 HOME으로 지정해야 하는데 다른 문서에도 이렇게 나와 있긴 하지만 권한 문제 때문인지(/var/task는 읽기 전용이다.) 제대로 되지 않았다. 그래서 /tmpHOME으로 지정하고 Lambda가 실행될 때 S3에 올려둔 폰트를 다운로드 받도록 했다. 한글 폰트 하나는 4.5MB 정도이지만 많은 weight가 필요하거나 CJK 폰트를 모두 쓰려면 NotoSansCJK-Regular.ttc가 18.7MB이므로 zip 파일의 용량이 50MB가 넘어서 어차피 올릴 수가 없다.

// s3.js
const AWS = require('aws-sdk');
const fs = require('fs');

const s3 = new AWS.S3({ apiVersion: '2006-03-01' });

module.exports = {
  uploadFile: (buffer) => {
    // 생략
  },
  downloadFont: () => {
    return new Promise((resolve, reject) => {
      return s3.getObject({
        Bucket: 'outsider-demo',
        Key: 'NotoSansKR-Regular.otf'
      }, (err, data) => {
        if (err) { return reject(err); }

        fs.mkdir('/tmp/.fonts', (err) => {
          if (err) { return reject(err); }
          fs.writeFile('/tmp/.fonts/NotoSansKR-Regular.otf', data.Body, (err) => {
            if (err) { return reject(err); }
            resolve();
          });
        });
      });
    });
  },
};

S3 버킷에서 NotoSansKR-Regular.otf폰트를 다운받는 downloadFont()함수를 추가했다. 폰트는 미리 S3에 올려놨고 이 파일을 다운로드 받아서 /tmp/.fonts/에 저장한다.

// index.js
const { connectChrome } = require('./chrome');
const { uploadFile, downloadFont } = require('./s3');

exports.handler = (event, context, callback) => {
  downloadFont()
    .then(connectChrome)
    .then(uploadFile)
    .then(context.succeed)
    .catch(context.fail);
};

index.js에서 크롬을 실행하기 전에 폰트를 먼저 다운로드 받도록 했다.

환경변수를 변경해서 새 Lambda 함수의 배포 화면

압축한 function.zip을 다시 업로드 하고 HOME/tmp로 지정했다. 새로 업로드한 Lambda 함수를 다시 실행하면 다음과 같이 한글이 제대로 나타나는 걸 볼 수 있다.

한글이 제대로 나온 이 블로그의 스크린샷



댓글 쓰기

LambCI의 Docker 이미지로 AWS Lambda 함수 로컬에서 테스트하기

$
0
0

AWS Lambda를 사용할 때 불편한 점은 Lambda에 배포해야 테스트를 해볼 수 있다는 부분이다. 그동안 Lambda를 쓰면서 느낀 경험은 최대한 로직은 유닛테스트로 기능을 확인하고 Lambda의 핸들러에서는 간단히 이 로직은 호출만 하는 정도로 작성하면 로컬에서 테스트를 해보면서 코드를 작성할 수 있다. 코드를 수정할 때마다 Lambda에 배포하는 것은 매우 불편한 일이므로 로컬에서 테스트할 수 있으면 시간을 많이 줄일 수 있다.

Node.js이든 Python이든 간단한 코드면 유닛테스트로 충분하지만 좀 더 복잡하게 사용할 일이 있다면 Lambda에 배포를 해야 하므로 매우 귀찮을 일이 된다. 특히 얼마 전에 사용해 본 Headless Chrome을 Lambda에서 사용하는 작업은 Lambda 환경의 제약을 확인해 봐야 하므로 잦은 배포를 해야 해서 아주 귀찮을 일이었다. 특히 Lambda 함수의 용량이 크다면 배포할 때마다 시간도 걸리기 때문에 더욱 작업이 번거로운 일이 된다.

lambci/docker-lambda

LambCI는 Lambda에서 CI를 할 수 있게 하는 서버리스 솔루션인데 여기서 Lambda 환경을 거의 같게 만든 Docker를 제공하고 있다. 내가 작업한 것은 아니지만 LambCI는 nodejs-ko에서 배포용으로 쓰고 있어서 알고는 있었지만 docker-lambda는 모르고 있다가 얼마 전에 AWS의 Lambda를 로컬에서 테스트해볼 수 있게 하는 SAM Local을 보다가 여기서 로컬 테스트 용도로 LambCI의 docker-lambda 이미지를 사용한다는 것을 알게 되어 Docker만 사용해보았다.(sam-local의 경우 CloudFormation기반이라서 테스트는 해보지 않았다.)

테스트 목적으로 docker-lambda를 사용해봤는데 100% Lambda와 같지는 않아도 거의 비슷하게 환경이 구성되어 있으므로 로컬에서 쉽게 테스트해볼 수 있다. 사용해본 느낌으로는 몇 가지 차이점만 실제 AWS Lambda에 배포해서 테스트해보고 나머지는 docker-lambda에서 동작 여부를 확인해 보는 정도로 충분해서 아주 편했다. AWS Lambda에서 안 되는 경우 docker-lambda에서도 안 되는지 확인하고 docker-lambda에서 동작하도록 코드를 수정한 뒤에 AWS Lambda에 배포하는 식으로 작업하니까 시간을 꽤 줄일 수 있었다. 물론 docker-lambda에서는 동작하지만 다른 환경 문제고 AWS Lambda에서는 안 되는 경우가 없는 것은 아니다.

docker-lambda로 Lambda 함수 실행하기

Lambda 함수를 LambCI의 Docker 이미지로 테스트해볼 수 있는데 Docker Hub에 환경별로 올려져 있다. nodejs4.3, 'nodejs', 'nodejs6.10', 'python2.7', 'python3.6', 'java8' 등 Lambda에서 지원하는 환경이 모두 Docker 이미지로 존재한다.

Node.js를 사용한다고 할 때 다음과 같은 index.js가 있다고 해보자.

// index.js
console.log('starting function');
exports.handler = (event, context, callback) => {
  console.log('event:', event);
  context.succeed('hello world');
};

여기서는 코드의 내용이 중요하진 않으므로 간단한 Lambda 코드만 사용했다. 터미널에서 Docker 이미지로 다음과 같이 Lambda 함수를 실행할 수 있다.

$ docker run -v "$PWD":/var/task lambci/lambda:nodejs6.10
START RequestId: 543c5021-013e-17b4-ffea-e179e78c5b7b Version: $LATEST
2017-08-27T16:35:19.970Z  543c5021-013e-17b4-ffea-e179e78c5b7b  starting function
2017-08-27T16:35:19.972Z  543c5021-013e-17b4-ffea-e179e78c5b7b  event: {}
END RequestId: 543c5021-013e-17b4-ffea-e179e78c5b7b
REPORT RequestId: 543c5021-013e-17b4-ffea-e179e78c5b7b  Duration: 7.65 ms Billed Duration: 100 ms Memory Size: 1536 MB  Max Memory Used: 28 MB

"hello world"

위에서 보듯이 현재 폴더에 있는 Lambda 함수를 nodejs6.10환경에서 실행해 볼 수 있고 실제 AWS Lambda에서 볼 수 있는 로그처럼 실행 결과까지 모두 볼 수 있다. Node.js 6.10 환경이 아니라면 다른 이미지를 사용하면 되고 -v "$PWD":/var/task에서 볼 수 있듯이 현재 폴더를 Docker 내에서 실행하게 된다.

// dist.js
console.log('starting function');
exports.entry = (event, context, callback) => {
  console.log('event:', event);
  context.succeed('hello world');
};

위처럼 기본값인 index.jshandler함수를 사용하지 않고 dist.js파일에 entry라는 함수를 진입점으로 쓰고 싶다면 Docker 명령어 마지막에 dist.entry처럼 핸들러를 지정해 주면 된다.(AWS Lambda의 설정에서도 handler를 지정할 수 있다.)

docker run -v "$PWD":/var/task lambci/lambda:nodejs6.10 dist.entry
START RequestId: d5f50084-0ea8-1530-8183-dc2fce92b7b4 Version: $LATEST
2017-08-27T16:45:18.067Z  d5f50084-0ea8-1530-8183-dc2fce92b7b4  starting function
2017-08-27T16:45:18.069Z  d5f50084-0ea8-1530-8183-dc2fce92b7b4  event: {}
END RequestId: d5f50084-0ea8-1530-8183-dc2fce92b7b4
REPORT RequestId: d5f50084-0ea8-1530-8183-dc2fce92b7b4  Duration: 8.79 ms Billed Duration: 100 ms Memory Size: 1536 MB  Max Memory Used: 28 MB

"hello world"

지금까지는 입력 이벤트가 없이 실행했는데 입력 이벤트가 있는 경우에는 docker명령어 마지막에 JSON을 전달하면 된다.

$ docker run -v "$PWD":/var/task lambci/lambda:nodejs6.10 dist.entry '{ "name": "outsider" }'
START RequestId: b86d5ed1-293d-1be5-9fc6-fb87968e741c Version: $LATEST
2017-08-27T16:48:21.068Z  b86d5ed1-293d-1be5-9fc6-fb87968e741c  starting function
2017-08-27T16:48:21.070Z  b86d5ed1-293d-1be5-9fc6-fb87968e741c  event: { name: 'outsider' }
END RequestId: b86d5ed1-293d-1be5-9fc6-fb87968e741c
REPORT RequestId: b86d5ed1-293d-1be5-9fc6-fb87968e741c  Duration: 11.63 ms  Billed Duration: 100 ms Memory Size: 1536 MB  Max Memory Used: 28 MB

"hello world"

위 로그에서 보듯이 전달한 JSON이 출력된 것을 볼 수 있다.

Headless Chrome을 사용하면서 꽤 많은 테스트를 해보았을 때 Lambda 환경이 거의 그대로 구현되어 있어서 테스트를 상당히 편리하게 도와준다. 그리고 sam-local에서도 채택한 걸 보면 LambCI에서 제공하는 Docker 이미지가 신뢰할 수 있는 퀄리티를 제공하고 있다고 생각된다. AWS에서 직접 공식 이미지를 제공해 주면 아주 좋겠지만....

댓글 쓰기

기술 뉴스 #85 : 17-09-01

$
0
0

웹개발 관련

  • Introduction to Preact — a smaller, faster React alternative : React와 API 호환성이 있으면서 용량이 3KB밖에 안되는 Preact를 소개하는 글이다. Preact를 사용할 때 React와 어떻게 다른지를 소개하고 preact-compat으로 완전히 React와 호환되게 할 방법을 소개한다. 이 글에서는 간단한 앱에서는 Preact가 괜찮고 복잡한 앱이라면 React를 권하고 있다.(영어)
  • Understanding V8’s Bytecode : V8이 자바스크립트 코드를 최적화할 때 생성하는 Bytecode에 대해서 설명하고 Chrome에서 Bytecode를 출력하는 방법과 각 코드가 어떤 의미인지를 간단히 설명한다. Bytecode를 분석해 볼 일은 없지만 참고삼아 알아두면 좋을 정보다.(영어)
  • Increase your web development skill-set: 150 animated tips on Chrome DevTools : 크롬 개발자 도구에 대해 Dev Tips를 안내하는 글이다. 오랫동안 구독해서 보고 있던 뉴스레터인데 이 사이트에 가면 이미 올려진 150개 이상의 개발자도구 팁에 대한 간단한 GIF 애니메이션을 볼 수 있다.(영어)
  • Inside a super fast CSS engine: Quantum CSS (aka Stylo) : Firefox의 차세대 엔진인 Quantum 프로젝트의 Quantum CSS(구 Stylo)가 CSS 처리를 어떻게 빠르게 하는지를 설명한 글이다. 앞부분은 웹브라우저가 CSS를 웹페이지에 어떻게 적용하는지를 설명하고 있어서 브라우저가 동작하는 방식을 이해할 수 있고 이 부분에서 Quantum CSS가 어떻게 속도를 최적화하는지를 설명하고 있다.(영어)
  • Headless mode : Firefox도 56 버전부터 headless 모드를 지원한다.(영어)

그 밖의 프로그래밍 관련

  • Kubernetes at GitHub : GitHub에서 수년간 운영하던 인프라를 Kubernetes로 교체하는 과정을 정리한 글이다. 개발팀이 더 유연하게 운영할 수 있도록 오케스트레이션 도구에서 Kubernetes를 도입하고 테스트를 위해 브랜치를 AWS에 배포해서 리뷰용도로 사용할 수 있도록 AWS 상에서 테스트를 진행하고 이를 github 서비스에 일부분씩 적용해서 전체를 무 상태 서버 전체를 Kubernetes 기반으로 동작하도록 적용했다.(영어)
  • APIs as infrastructure: future-proofing Stripe with versioning : 온라인 결제서비스를 제공하는 Stripe에서 API 버전을 어떻게 관리하고 있는지를 설명한 글이다. 보통 URL에 v1, v2처럼 버전을 붙이고 Stripe도 이를 사용하고 있지만, 하위호환성이 깨지는 문제를 막기 위해 변경이 있는 릴리스 날짜를 버전으로 사용하고 사용자가 처음 API를 사용하면 해당 날짜 버전에 고정되어 계속 같은 버전을 사용하고 변경사항을 자동 생성한 버전 변경 모듈을 이용해서 현재 API에서 사용자가 원하는 버전의 API의 호환성을 자동으로 맞춰주는 방식을 사용한다.(영어)
  • Android – 8.0 Oreo : 안드로이드 8.0 Oreo가 나왔다.(영어)

볼만한 링크

  • [카카오AI리포트]머신러닝 적용의 실제 : 머신러닝을 적용할 때 빠지기 쉬운 함정이나 실제로 고려해야 할 부분을 정리한 글이다. 머신러닝이나 딥러닝을 자세히 알지는 못하지만, 실제 업무에서 데이터를 분석할 때 왜 한 번만 분석하지 말고 계속 지켜봐야 하는지, 신선한 데이터가 왜 중요한지, 어떤 상황에서 데이터 편향이 일어날 수 있는지 잘 정리되어 있다. 실제 적용할 때 이 글을 보고 찬찬히 고민해 볼 여지가 많이 있어 보인다.(한국어)
  • 대기업 Aaron과 실리콘밸리 Bryan : 두 명의 가상 개발자가 대기업과 스타트업으로 서로 이직을 하면서 Role-driven 조직과 Rank-driven 조직에서 최고의 인재가 어떻게 최악의 인재가 되는지를 비교해 준 글이다. 예시가 재미있고 조직 문화와 개개인의 특성이 맞아야 업무 효율도 난다고 생각하는 편이라 공감하면서 읽었다.(한국어)
  • 코드로 100명 이상의 네임택 한 번에 디자인하기 : Design Spectrum 행사를 준비하면서 100명 참가자의 네임택을 만들기 위해서 Sketch의 JavaScript API로 디자인한 네임택에 이름을 넣어 100개를 만들고 A4마다 나눠서 출력한 과정을 담고 있다. Skectch에 JavaScript API가 생긴 줄도 모르고 있었던지라 다음에 필요한 일이 생기면 비슷하게 해보고 싶다.(한국어)

IT 업계 뉴스

  • 우버 새 CEO에 '다라 코스로샤히' 익스피디아 CEO : Uber의 새 CEO로 익스피디아의 CEO인 다라 코스로샤히로 결정되었다.(한국어)
  • Node.js has forked into Ayo : Node.js가 Ayo란 프로젝트로 포크되었다. 국내 기사는 제대로 정리가 안 되어 있는 거 같아서 영문을 링크했는데 이 사건의 내용을 완전히 파악하기가 좀 쉽지는 않은데 현재 TSC를 이끄는 Rod Vagg가 Node.js의 Code of Conduct를 어겼다고 불만이 제기되어 Rod를 TSC에서 제외하는 안건이 CTC에 올라갔지만 6:4로 부결되었다. 이에 반대한 4명은 Node.js에서 빠지기로 하고(참고 글 1, 2) 아마 이들을 중신으로 Ayo가 진행되는 것으로 예상한다. 이 이슈에 대한 Rod의 입장이 장문의 글로 올라왔고 아직도 진행 중인 상황이다. 개인적으로는 이전 io.js 사건 만큼 심각하게 보고 있지는 않고 현재 CTC나 TSC에서 문제를 잘 해결할 거라고 기대하고 있다.(영어)

프로젝트

  • Puppeteer : Headless Chrome을 제어할 수 있는 Node.js 라이브러리.
  • OpenFaaS : Docker와 Kubernetes로 Functions as a Service를 사용할 수 있는 서버리스 프레임워크.
  • Wekan : Node.js로 작성된 Trello같은 오픈소스 칸반.
  • rendertron : Headless Chrome으로 웹사이트를 렌더링하거나 스크린샷을 찍을 수 있는 솔루션.
  • Feather : 오픈소스 아이콘 패키지.
  • Workbox : PWA의 오프라인 캐싱을 도와주는 JavaScript 라이브러리.
  • Saucs : 사용하는 제품이나 벤더를 구독하면 취약점을 알려주는 서비스.

버전 업데이트

댓글 쓰기


Terraform의 plan 결과를 저장해서 사용하기

$
0
0

Terraform으로 리소스를 관리하다 보면 자연히 관리하는 리소스가 많아지게 된다. 하나의 state 파일에서 너무 많은 리소스를 관리하지 않기를 권장하고 있지만, 너무 나누면 관리가 어려운 부분도 있고 성격상 어쩔 수 없이 많은 리소스가 생기게 되는 때도 있다. 이렇게 관리 리소스가 많았지만 plan이나 apply를 할 때 Terraform이 tfstate에 기록해 둔 내용과 AWS 같은 클라우드에 실제 구성을 비교해봐야 하므로 꽤 많은 시간이 걸리게 된다.

기본적으로 planapply를 하면 tfstate파일과 실제 상태를 비교하는 작업을 항상 진행하므로 아무리 간단한 수정을 하더라도 시간이 꽤 많이 걸리게 되고 항상 terraform plan을 한 뒤 terraform apply를 하게 되는데 양쪽 모두에서 이 작업을 하게 되므로 시간은 더욱 많이 걸리게 된다.

terraform plan -out=path

다음은 AWS Route53에 CNAME을 하나 추가하고 plan을 실행한 결과이다.

$ terraform plan
Acquiring state lock. This may take a few moments...
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

data.terraform_remote_state.vpc: Refreshing state...
data.terraform_remote_state.us_east_1: Refreshing state...
data.terraform_remote_state.ecs_services: Refreshing state...
aws_iam_user.apex-basic: Refreshing state... (ID: apex-basic)
aws_dynamodb_table.terraform-lock: Refreshing state... (ID: SideEffectTerraformStateLock)
aws_cloudfront_origin_access_identity.labs_sideeffect_kr: Refreshing state... (ID: E2WUVSG763JX3V)
data.aws_elb_service_account.main: Refreshing state...
data.aws_acm_certificate.sideeffect_kr: Refreshing state...
data.aws_iam_policy_document.ecs_service_role: Refreshing state...
data.aws_iam_policy_document.config_service: Refreshing state...
aws_s3_bucket.nodejs-ko: Refreshing state... (ID: nodejs-ko)
aws_route53_zone.sideeffect_kr: Refreshing state... (ID: Z12CBQ5AHVFFYA)
aws_iam_group.apex: Refreshing state... (ID: apex)
data.aws_iam_policy_document.ecs_instance_role: Refreshing state...
aws_cloudwatch_log_group.ap_northeast_1_ecs: Refreshing state... (ID: ap-northeast-1-ecs)
data.aws_iam_policy_document.nodejs-ko-twitter_lambda_logs: Refreshing state...
data.aws_iam_policy_document.apex-default: Refreshing state...
aws_iam_user.outsider: Refreshing state... (ID: outsider)
aws_cloudfront_origin_access_identity.nodejs_sideeffect_kr: Refreshing state... (ID: E25WJ3C3LDATWV)
data.aws_iam_policy_document.nodejs-ko-twitter_lambda_function: Refreshing state...
data.aws_iam_policy_document.vault_ecs_task: Refreshing state...
data.aws_iam_policy_document.aws_s3_bucket_logs: Refreshing state...
aws_iam_role.config_service: Refreshing state... (ID: config-service)
aws_iam_role.ecs_service_role: Refreshing state... (ID: ecsServiceRole)
aws_iam_policy.nodejs-ko-twitter_lambda_logs: Refreshing state... (ID: arn:aws:iam::410655858509:policy/nodejs-ko-twitter_lambda_logs)
aws_iam_role.ecs_instance_role: Refreshing state... (ID: ecsInstanceRole)
data.aws_iam_policy_document.labs_sideeffect_kr: Refreshing state...
aws_iam_policy.apex-default: Refreshing state... (ID: arn:aws:iam::410655858509:policy/apex-default)
aws_iam_group_membership.apex: Refreshing state... (ID: apex-group-membership)
aws_iam_role.nodejs-ko-twitter_lambda_function: Refreshing state... (ID: nodejs-ko-twitter_lambda_function)
aws_iam_role.vault_ecs_task: Refreshing state... (ID: vault-ecs-task)
aws_s3_bucket.logs: Refreshing state... (ID: kr.sideeffect.logs)
aws_config_configuration_recorder.ap_northeast_1: Refreshing state... (ID: aws-config-ap-northeast-1)
aws_iam_policy_attachment.AWSConfigRole-policy-attachment: Refreshing state... (ID: AWSConfigRole-policy-attachment)
aws_iam_policy_attachment.AdministratorAccess-policy-attachment: Refreshing state... (ID: AdministratorAccess-policy-attachment)
aws_iam_policy_attachment.IAMFullAccess-policy-attachment: Refreshing state... (ID: IAMFullAccess-policy-attachment)
aws_iam_policy_attachment.AWSLambdaFullAccess-policy-attachment: Refreshing state... (ID: AWSLambdaFullAccess-policy-attachment)
data.aws_iam_policy_document.nodejs_sideeffect_kr: Refreshing state...
aws_s3_bucket.labs_sideeffect_kr: Refreshing state... (ID: kr.sideeffect.labs)
aws_route53_record.vault_sideeffect_kr: Refreshing state... (ID: Z12CBQ5AHVFFYA_vault.sideeffect.kr_A)
aws_route53_record.www_sideeffect_kr: Refreshing state... (ID: Z12CBQ5AHVFFYA_www.sideeffect.kr_A)
aws_iam_policy_attachment.ecs_service_role: Refreshing state... (ID: AmazonEC2ContainerServiceforEC2Role)
aws_iam_policy_attachment.ecs_instance_role: Refreshing state... (ID: AmazonEC2ContainerServiceforEC2Role)
aws_iam_instance_profile.ecs_instance_role: Refreshing state... (ID: ecsInstanceRole)
aws_iam_policy_attachment.AmazonS3FullAccess-policy-attachment: Refreshing state... (ID: AmazonS3FullAccess-policy-attachment)
aws_config_config_rule.ap_northeast_1_cloudtrail_enabled: Refreshing state... (ID: cloudtrail-enabled)
aws_config_config_rule.ap_northeast_1_ec2_instances_in_vpc: Refreshing state... (ID: ec2-instances-in-vpc)
aws_s3_bucket.nodejs_sideeffect_kr: Refreshing state... (ID: kr.sideeffect.nodejs)
aws_iam_policy_attachment.apex-default-policy-attachment: Refreshing state... (ID: apex-default-policy-attachment)
aws_iam_policy_attachment.nodejs-ko-twitter_lambda_logs-policy-attachment: Refreshing state... (ID: nodejs-ko-twitter_lambda_logs-policy-attachment)
aws_route53_record.sideeffect_kr: Refreshing state... (ID: Z12CBQ5AHVFFYA_sideeffect.kr_A)
aws_s3_bucket.terraform-state: Refreshing state... (ID: kr.sideeffect.terraform.state)
aws_cloudtrail.ap_northeast_1: Refreshing state... (ID: ap-northeast-1)
aws_config_delivery_channel.ap_northeast_1: Refreshing state... (ID: aws-config-ap-northeast-1)
aws_s3_bucket.vault_sideeffect_kr: Refreshing state... (ID: kr.sideeffect.vault)
data.aws_iam_policy_document.config_service_delivery_permission: Refreshing state...
aws_iam_policy.config_service_delivery_permission: Refreshing state... (ID: arn:aws:iam::410655858509:policy/config-service-delivery-permission)
aws_config_configuration_recorder_status.ap_northeast_1: Refreshing state... (ID: aws-config-ap-northeast-1)
aws_cloudfront_distribution.labs_sideeffect_kr: Refreshing state... (ID: E3EP48YZYH5MDL)
aws_iam_policy_attachment.config_service_delivery_permission_attachment: Refreshing state... (ID: config-service-delivery-permission-attachment)
aws_cloudfront_distribution.nodejs_sideeffect_kr: Refreshing state... (ID: E3AG1R3KSY57S4)
aws_route53_record.labs_sideeffect_kr: Refreshing state... (ID: Z12CBQ5AHVFFYA_labs.sideeffect.kr_A)
aws_route53_record.nodejs_sideeffect_kr: Refreshing state... (ID: Z12CBQ5AHVFFYA_nodejs.sideeffect.kr_A)
data.aws_iam_policy_document.vault: Refreshing state...
aws_iam_policy.vault: Refreshing state... (ID: arn:aws:iam::410655858509:policy/vault-to-write-s3)
aws_iam_policy_attachment.vault-policy-attachment: Refreshing state... (ID: vault-policy-attachment)
The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed. Cyan entries are data sources to be read.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ aws_route53_record.demo_sideeffect_kr
    fqdn:               "<computed>"
    name:               "demo.sideeffect.kr"
    records.#:          "1"
    records.3313064457: "blog.outsider.ne.kr"
    type:               "CNAME"
    zone_id:            "Z12CBQ5AHVFFYA"


Plan: 1 to add, 0 to change, 0 to destroy.
Releasing state lock. This may take a few moments...

좀 길지만, 전체 출력을 모두 적었다. Route53에 CNAME을 생성한 것이지만 tfstate에 있는 모든 리소스를 다 검사한다. 상단에 나오는 출력 부분은 모두 이 부분의 상태를 검사하는 부분이고 최종적으로 CNAME을 1개 추가한다고 알려준다.

$ terraform apply
... 중략
aws_cloudfront_distribution.labs_sideeffect_kr: Refreshing state... (ID: E3EP48YZYH5MDL)
aws_config_configuration_recorder_status.ap_northeast_1: Refreshing state... (ID: aws-config-ap-northeast-1)
aws_cloudfront_distribution.nodejs_sideeffect_kr: Refreshing state... (ID: E3AG1R3KSY57S4)
aws_iam_policy_attachment.config_service_delivery_permission_attachment: Refreshing state... (ID: config-service-delivery-permission-attachment)
aws_route53_record.labs_sideeffect_kr: Refreshing state... (ID: Z12CBQ5AHVFFYA_labs.sideeffect.kr_A)
aws_route53_record.nodejs_sideeffect_kr: Refreshing state... (ID: Z12CBQ5AHVFFYA_nodejs.sideeffect.kr_A)
data.aws_iam_policy_document.vault: Refreshing state...
aws_iam_policy.vault: Refreshing state... (ID: arn:aws:iam::410655858509:policy/vault-to-write-s3)
aws_iam_policy_attachment.vault-policy-attachment: Refreshing state... (ID: vault-policy-attachment)
aws_route53_record.demo_sideeffect_kr: Creating...
  fqdn:               "" => "<computed>"
  name:               "" => "demo.sideeffect.kr"
  records.#:          "" => "1"
  records.3313064457: "" => "blog.outsider.ne.kr"
  ttl:                "" => "5"
  type:               "" => "CNAME"
  zone_id:            "" => "Z12CBQ5AHVFFYA"
aws_route53_record.demo_sideeffect_kr: Still creating... (10s elapsed)
aws_route53_record.demo_sideeffect_kr: Still creating... (20s elapsed)
aws_route53_record.demo_sideeffect_kr: Still creating... (30s elapsed)
aws_route53_record.demo_sideeffect_kr: Still creating... (40s elapsed)
aws_route53_record.demo_sideeffect_kr: Still creating... (40s elapsed)
aws_route53_record.demo_sideeffect_kr: Creation complete (ID: Z12CBQ5AHVFFYA_demo.sideeffect.kr_CNAME)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path:
Releasing state lock. This may take a few moments...

이를 적용하기 위해서 terraform apply를 실행하면 plan과 마찬가지로 다시 리소스를 모두 검사한다. 이는 planapply를 실행하는 사이에 변경된 부분이 있을 수 있으므로 다시 검사를 다 실행하는 것인데 이 리소스가 많아질수록 점점 고통스러운 시간이 되고 그 결과 실패한다면 수정하고 다시 해야 하므로 귀찮아지게 된다.

앞에서 plan을 할 때 출력 로그를 보면 다음과 같은 문구가 있다.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

그냥 넘어가기 쉬운데 plan할 때 -out파리미터로 플랜을 저장하지 않아서 apply할 때 똑같이 실행된다고 보장할 수 없다는 부분이다. 이는 앞에서 apply 할 때 재검사하는 것과 같은 이유인데 plan한 뒤에 다른 변경사항이 생긴다면 적용할 때 다른 변경사항(혹은 다른 사람이 만든 결과를 삭제하는..)이 적용될 수도 있다.

terraform plan -out=planfile처럼 -out파라미터로 플랜파일을 지정하면(이름은 원하는 대로 지정할 수 있다.)

$ terraform plan -out=planfile
// 중략
Path: planfile

+ aws_route53_record.demo_sideeffect_kr
    fqdn:               "<computed>"
    name:               "demo.sideeffect.kr"
    records.#:          "1"
    records.3313064457: "blog.outsider.ne.kr"
    ttl:                "5"
    type:               "CNAME"
    zone_id:            "Z12CBQ5AHVFFYA"


Plan: 1 to add, 0 to change, 0 to destroy.
Releasing state lock. This may take a few moments...

Path: planfile에서 보듯이 저장한 플랜파일을 알려주고 이 결과가 이 파일에 저장이 된다. 이를 적용할 때는 terraform apply planfile로 앞에서 만든 파일을 지정해 주면 된다.

$ terraform apply planfile
Acquiring state lock. This may take a few moments...
Releasing state lock. This may take a few moments...
Acquiring state lock. This may take a few moments...
aws_route53_record.demo_sideeffect_kr: Creating...
  fqdn:               "" => "<computed>"
  name:               "" => "demo.sideeffect.kr"
  records.#:          "" => "1"
  records.3313064457: "" => "blog.outsider.ne.kr"
  ttl:                "" => "5"
  type:               "" => "CNAME"
  zone_id:            "" => "Z12CBQ5AHVFFYA"
aws_route53_record.demo_sideeffect_kr: Still creating... (10s elapsed)
aws_route53_record.demo_sideeffect_kr: Still creating... (20s elapsed)
aws_route53_record.demo_sideeffect_kr: Still creating... (30s elapsed)
aws_route53_record.demo_sideeffect_kr: Still creating... (40s elapsed)
aws_route53_record.demo_sideeffect_kr: Creation complete (ID: Z12CBQ5AHVFFYA_demo.sideeffect.kr_CNAME)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path:
Releasing state lock. This may take a few moments...

plan에서 검사한 결과를 저장했으므로 위처럼 planfile을 지정해서 apply하면 다른 리소스를 재검사하지 않고 바로 plan한 내용만 적용하게 되고 plan한 부분을 적용함을 보장할 수 있다.

댓글 쓰기

Terraform에서 plan을 실행할 때 대상을 한정하기

$
0
0

Terraformplan, apply를 할 때 결과도 보장할 수 있고 시간도 단축할 수 있는 Terraform의 plan 결과를 저장해서 사용하기에 대해서 올렸는데 이 글에서도 설명했듯이 관리하는 리소스가 많아지면 plan을 할 때 많은 시간이 걸리는 것은 어쩔 수 없다. -out옵션을 사용하면 apply의 시간을 꽤 줄일 수 있지만 plan시간은 줄일 수 없다.

terraform plan -target=path

똑같이 Route53에 레코드를 하나 추가해 보자.

resource "aws_route53_record" "demo_sideeffect_kr" {
  zone_id = "${aws_route53_zone.sideeffect_kr.zone_id}"
  name = "demo.sideeffect.kr"
  type = "CNAME"
  ttl = "5"
  records = ["example.com"]
}

내용은 크게 중요치 않고 aws_route53_recorddemo_sideeffect_kr라는 이름의 리소스를 추가한 것이다. 이 내용을 추가하고 terraform plan을 실행하면 이 폴더에 포함된 tfstate에서 관리하는 리소스를 모두 검사한다. 로그가 길어서 여기에는 다시 남기지 않겠는데 이전 글에서 plan을 한 결과를 보면 된다.

내 개인 terraform에서는 관리하는 리소스가 많지 않아서 그리 오래 걸리지 않지만, 회사에서는 plan을 하면 10분 가까이 걸리는 것도 있다.

이때 -target=resource옵션을 사용하면 해당 리소스와 의존관계에 있는 리소스만 검사를 하게 된다. 여기서 Route53에 레코드를 추가한 것처럼 이 tfstate에서 S3나 CloudWatch 등도 관리하고 있을 수 있지만 여기서 추가한 부분이 명확하므로 굳이 다른 부분까지 plan을 해서 비교를 해볼 필요가 없다.

$ terraform plan -target=aws_route53_record.demo_sideeffect_kr
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.

aws_route53_zone.sideeffect_kr: Refreshing state... (ID: Z12CBQ5AHVFFYA)
The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed. Cyan entries are data sources to be read.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ aws_route53_record.demo_sideeffect_kr
    fqdn:               "<computed>"
    name:               "demo.sideeffect.kr"
    records.#:          "1"
    records.3313064457: "blog.outsider.ne.kr"
    ttl:                "5"
    type:               "CNAME"
    zone_id:            "Z12CBQ5AHVFFYA"


Plan: 1 to add, 0 to change, 0 to destroy.

위처럼 -target옵션으로 리소스를 한정해 주면 지정한 리소스만 상태를 검사한 뒤에 plan결과를 나오는 것을 볼 수 있고 시간도 당연히 줄어든다. 물론 -target옵션은 여러 번 사용해서 다수의 대상을 지정하는 것도 가능하다.

댓글 쓰기

기술 뉴스 #86 : 17-09-15

$
0
0

웹개발 관련

  • Angular 2 대신에 Vue.js를 선택한 이유 (그리고 React를 선택하지 않은 이유) : Why we moved from Angular 2 to Vue.js(and why we didn’t choose React)의 번역 글로 Angular 2를 사용하다가 Vue.js를 바꾸면서 고민한 과정을 설명하고 있다. Angular 2에 불만을 느끼면서 다음 프레임워크로 React와 Vue.js를 비교하면서 프로토타이핑을 해보고 배우기 쉽고 라이센스 이슈 등 실제로 고민할 만한 내용을 설명하고 있다. Vue.js에 동의하지 않더라도 고민 과정을 보는 것도 의미가 있다.(한국어)
  • A Guide To Virtual Reality For Web Developers : WebVR을 이용한 웹 브라우저에서의 VR 지원에 대해서 현재 상황을 정리한 글이다. 소스코드를 제공하기보다는 WebVR이 현재 각 기기와 컨트롤러를 어떻게 지원하고 있고 속도 문제 등으로 콘텐츠를 동적으로 로딩하거나 requestIdleCallback로 렌더링 상태를 추적해서 그래픽 퀄리티를 자동 조절하는 등 주의해야 할 내용도 설명하고 있다. WebVR을 구현할 일이 현재 많을지는 모르겠지만 어떤 상태에 있는지 파악하기 좋은 글이다.(영어)
  • Betting on the Web : 왜 PWA를 선택했는지를 설명한 글이다. 시작부터 개발자가 기술에 투자하는 이유 등 주제보다 너무 거창하게 얘기하고 있긴 하지만 Web은 완전히 오픈되고 아주 큰 플랫폼이고 앱이 가지는 단점(설치, 크기 등)과 비교했을 때 PWA가 우위에 있는 점을 설명하고 있다. 이 글쓴이는 최근 공개된 스타벅스 PWA개발에 참여했고 146MB인 스타벅스 앱에 비해 PWA는 600KB밖에 안된다고 한다.(영어)

그 밖의 프로그래밍 관련

  • 자습해도 모르겠던 딥러닝, 머리속에 인스톨 시켜드립니다. : 제목 그대로 하용호 님이 딥러닝의 개념을 알려주는 발표자료이다. 하용호 님의 자료가 항상 그렇듯이 아주 쉽게 설명하기 때문에 딥러닝에 대해서 거의 모르는데 이 발표자료에서 그 복잡한 개념의 핵심 부분만 이해할 수 있도록 바로 얘기해주는 것처럼 잘 정리가 되어 있다. 딥러닝을 하고 있진 않아서 완전히 다 이해는 못 했지만 딥러닝 코드를 작성하기 위해 알아야 할 개념의 큰 그림을 설명해 주기 때문에 대충 어떤 흐름으로 흘러가는지 알 수 있고 나중에 딥러닝을 공부하기 전에 꼭 다시 봐야 할 자료라고 생각된다.(한국어)
  • Naver 오픈소스 가이드 : 네이버에서 대학생을 대상으로 오픈소스 참여를 하는 방법을 알려주는 가이드 문서를 공개했다. 오픈 소스의 저작권과 프로젝트를 공개하는 방법 등을 설명하고 컨트리뷰션을 하는 방법과 이유 등을 소개하고 있다.(한국어)
  • AWS VPC basic : AWS 인프라의 기본이 되는 VPC에서 알아야 하는 각 요소과 개념을 쉽게 설명한 글이다. 개발자가 네트워크 레벨부터 인프라를 구축하는 일이 많지는 않으므로 처음 VPC를 만들면 어떻게 구성해야 하는지도 몰라서 일단 동작하도록만 만드는 경우가 많은데 VPC는 인프라 보안의 기반이므로 반드시 알아두어야 한다고 생각한다. 이 글을 통해서 기본 개념을 잡을 수 있다.(한국어)

볼만한 링크

IT 업계 뉴스

프로젝트

  • AWX : Ansible Tower의 오픈소스 버전으로 Red Hat에서 스폰서로 참여하고 있다.
  • Native Navigation : Airbnb에서 공개한 React Native용 네비게이션 라이브러리로 현재 베타 상태이다.
  • ATOM IDE : GitHub에서 Facebook과 협업을 통해 텍스트에디터가 아닌 아톰 기반의 IDE를 발표했다.
  • fastify : Node.js 웹 프레임워크로 처리 속도가 빠르다고 벤치마크 결과를 홍보하고 있다.
  • Rythm.js : 소리에 맞춰서 애니메이션 효과를 줄 수 있는 JavaScript 라이브러리.

버전 업데이트

댓글 쓰기

Terraform 0.10.x에서 분리된 Provider의 사용

$
0
0

지난 8월 Terraform이 0.10 버전으로 올라왔다. 0.10에서는 몇 가지 큰 변경사항이 생겼는데 가장 대표적인 부분이 Provider의 분리이다.

별도로 관리되는 Provider

이전 버전까지는 Terraform안에 모든 Provider가 모두 담겨 있었다. 여기서 Provider는 AWS, Google Cloud Platform, GitHub, InfluxDB 등 Terraform으로 관리하고자 하는 서비스를 의미한다. 이 중에는 AWS나 GCP처럼 대형 클라우드도 있고 아주 간단한 서비스도 있지만, 현재는 60개 이상의 Provider가 존재한다.

HashiCorp에서 밝힌 내용에 따르면 Provider가 70개가 넘어가면서 각 서비스에 새로운 기능이 나와도 사용자들은 Terraform의 새로운 버전을 항상 기다려야 하고 또 이 복잡도로 인해서 Terraform에 버그가 생길 가능성도 커지고 있다. 예를 들어 Amazon이 re:invent에서 새로운 AWS 서비스를 발표하면 사용자들은 이 기능을 Terraform에 추가해 달라고 요청하고 이 기능을 사용하려면 Terraform의 새 버전이 나와야 한다는 것이다.

그래서 0.10부터는 Provider를 모두 별도의 플러그인으로 분리하고 Terraform은 그 자체의 핵심 기능만 제공하기로 했다.그래서 Terraform을 본연의 기능에 충실하고 각 Provider에 새로운 기능이나 버그에 대해서는 각 플러그인이 해결하고 릴리스함으로써 더 유연하게 대처할 수 있게 된 것이다. 이 Provider를 제공하는 GitHub 조직을 terraform-providers라고 새로 만들고 여기서 각 Provider의 개발과 릴리스를 진행하고 있다. Provider 버전과 관련한 최근 글을 보면 SemVer따르기로 했고 현재는 초기 개발 단계인 0.x.x 버전에 있지만 각 Provider 팀에서 어느 정도 준비가 되면 1.0.0을 릴리스 할 것이라고 한다.

Terraform 0.10의 사용

현재 Terraform 최신 버전은 0.10.5이다. 아무것도 없는 상황에서 다음과 같이 S3 버킷을 AWS에 만드는 Terraform 구성 파일을 만든다고 해보자.

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

resource "aws_s3_bucket" "kr_ne_outsider_test" {
  bucket = "kr.ne.outsider.test"
  acl    = "private"
}

0.9.x 까지는 여기서 terraform plan을 하면 결과가 나오지만, 이제는 오류가 발생한다.

$ terraform plan
Plugin reinitialization required. Please run "terraform init".
Reason: Could not satisfy plugin requirements.

Plugins are external binaries that Terraform uses to access and manipulate
resources. The configuration provided requires plugins which can't be located,
don't satisfy the version constraints, or are otherwise incompatible.

1 error(s) occurred:

* provider.aws: no suitable version installed
  version requirements: "(any version)"
  versions installed: none

Terraform automatically discovers provider requirements from your
configuration, including providers used in child modules. To see the
requirements and constraints from each module, run "terraform providers".

error satisfying plugin requirements

플러그인 요구사항을 만족하게 하지 못했으므로 terraform init을 실행하라는 문구이다. 앞에서 설명한 대로 Terraform Provider가 분리되었으므로 terraform init으로 이를 받아와야 한다. 이는 Terraform이 자동으로 추적하는데 provider "aws"이 아니더라도 설정에서 AWS 리소스를 사용하고 있으므로 aws Provider가 필요하다. 해당 설정에서 필요한 Provider 정보를 보고 싶으면 terraform providers를 실행하면 된다. 뒤에서 설명하겠지만 provider의 버전을 명시하면 여기에 지정한 버전 조건이 같이 나온다.

$ terraform providers
.
└── provider.aws

이제 terraform init으로 플러그인을 설치해보자.

$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (0.1.4)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 0.1"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

AWS Provider 0.1.4가 설치된 것을 볼 수 있다. 따로 Provider의 버전을 지정하지는 않았으므로 최신 버전을 가져와서 설치한다.

설치하면 .terraform폴더 아래 구조로 설치가 된다.(이 폴더는 git에서 관리할 필요가 없다.)

.terraform/
└── plugins
  └── darwin_amd64
    ├── lock.json
    └── terraform-provider-aws_v0.1.4_x4


Provider 버전 지정

경험상 Provider의 버전마다 지원 내용이 다르고 Terraform이 인프라를 다루고 있으므로 약간 조심스럽게 다루는 편이 좋다고 생각한다. 같이 Terraform을 사용하는 사람 간에 Provider의 버전을 맞추려면 다음과 같이 버전을 지정할 수 있다.

provider "aws" {
  version = "~> 0.1.4"
  region = "ap-northeast-1"
}

이렇게 하면 0.1.4 버전 이상의 버전을 사용하게 되고 버전을 특정 버전으로 고정하려면 version = "= 0.1.4"처럼 사용하면 된다.

Provider 업데이트

이렇게 사용해보자 Provider를 사용하다가 새로운 버전이 나왔을 때 어떻게 되는가가 궁금해졌다. 현재 AWS Provider의 최신 버전은 0.1.4이지만 강제로 0.1.2를 설치했다.

.terraform/
└── plugins
  └── darwin_amd64
    ├── lock.json
    └── terraform-provider-aws_v0.1.2_x4

설치된 버전을 확인하는 명령어는 몰라서 폴더를 보면 설치된 버전을 알 수 있다. 이 상태에서 terraform init을 해도 새 버전을 설치하거나 하진 않는다.

$ terraform init

Initializing provider plugins...

Terraform has been successfully initialized!

문서를 보면 이미 Provider가 설치된 경우 설치된 것을 그대로 사용하고 이를 업그레이드하려면 terraform init -upgrade를 사용하라고 되어 있다.

$ terraform init -upgrade

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (0.1.4)...

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

로그를 보면 새 버전을 다운로드 받는 것을 볼 수 있다. 폴더를 열어보면 새 버전으로 업그레이드가 되어 있다.

.terraform/
└── plugins
  └── darwin_amd64
    ├── lock.json
    └── terraform-provider-aws_v0.1.4_x4


기존에 0.9.x 이하에서 관리하던 Terraform

기존 0.9.x 이하 버전의 Terraform으로 관리하던 부분이 있어도 마이그레이션의 크게 어려움은 없다. 개인적으로 관리하는 Terraform에서도 같게 init을 하고 프로바이더를 설치한 뒤에 plan을 하자 바로 사용할 수 있었다.

$ terraform providers
.
├── provider.aws
└── provider.terraform

provider.terraform.tfstate를 원격으로 관리하기 위함이다.

댓글 쓰기

[Book] 코드로 인프라 관리하기

$
0
0
코드로 인프라 관리하기

코드로 인프라 관리하기 - 6점
키프 모리스 지음
강재준 옮김
한빛미디어


인프라 관련 스터디에서 주제로 선택해서 읽은 책이었다. 작년부터 클라우드 인프라를 만지기 시작하면서 Terraform을 쓰고 있지만 원래 인프라를 쭉 하던 개발자는 아니라서 제대로 공부를 하진 않았기 때문에 책은 나오자마자 샀지만 안 읽고 있다가 이번에 스터디를 계기로 다 읽었다.

서버를 애완동물이 아닌 가축처럼 취급하라.

AWS를 엄청 잘 만지는 건 아니고 인프라를 오래 연구한 것도 아니지만 어느 정도 지식은 가지고 있었다. 팀 내에서 의논해서 인프라를 어떤 방향으로 구축해 갈지를 1년 정도 논의해 오기도 했고 이전과는 다른 개념인 Infrastructure as Code를 적용할 때 현실적인 문제들의 해결이 될 실마리를 이 책이 제공해주길 바랐다. 결과부터 말하면 나로서는 실망이다. 전체를 다 읽고 느낀 점은 책이 너무 추상적이다. 처음부터 너무 쉬운 이론 얘기 들이 나와서 "다음 장 가면 실무 얘기 나오겠지..."하다가 책이 끝났다. 인프라 혹은 Infrastructure as Code에 대한 개념이 전혀 없다면 도움이 될 수도 있겠지만 아무래도 이쪽에 관심 있는 사람들은 사전지식을 꽤 많이 갖고 있지 않나 싶은데 이 책은 너무 이론적이라서 뻔한 말만 하다가 끝난 느낌이다.

이 책이 말하고자 하는 것은 클라우드 시대가 오면서 기존에 진짜 서버를 직접 관리하던 전통방식과는 완전히 달라졌고 여기에 더해 코드로 관리하는 접근이 왜 필요하고 어떻게 해야 하는지를 설명하고 있다. 결론부터 얘기하다 보니 좀 박하게 평했는데 좀 더 자세히 얘기하자면 초기에는 왜 코드로의 접근이 필요하고 어떤 도구들이 있는지 이를 적용하는 과정이나 개념은 어떻게 되는지를 조목조목 설명한다. 결국, 코드로 접근하다 보니 그동안 프로그래밍에서 애자일에서 하던 빠른 실패, 빠른 이터레이션, 완벽한 설계보다는 빠른 변경, 자동화 같은 개념을 그대로 인프라를 대상으로 설명하고 있다.개발자가 아닌 사람들을 대상으로 설명한다면 (인프라라는 관점에서) 신선한 접근일 수도 있지만, 개발자로서는 굳이 안 해도 되는 설명을 하고 있고 일부 부분에 대해서는 인프라라는 말을 빼면 프로그래밍에 관한 얘기인지 인프라에 관한 얘기인지 모를 정도이다.

1장 문제와 원칙

클라우드 시대의 동적 인프라에서 우리가 겪는 문제, 서버 폭증, 눈송이 서버, 구성 편차, 자동화 공포 등을 설명한다. 이를 해결하기 위해 정의파일을 사용하고 문서화 및 버전 관리를 함으로써 해결하는 접근을 설명한다.

철기 시대와 클라우드 시대의 근본적인 차이는 '매우 신뢰할 수 있는 하드웨어 위에서 동작하던 신뢰할 수 없는 소프트웨어'가 '신뢰할 수 없는 하드웨어 위에서 신뢰성 있게 동작하는 소프트웨어'로 바뀐 것이다.

시스템을 안전하고 빠르게 변경하는 가장 좋은 방법은 구성을 자주 변경해보는 것이다.

2장 동적 인프라 플랫폼

코드로 인프라를 관리하기 위해 플랫폼이 갖추어야 할 특성을 설명하고 있다. 코드로 관리해야 하므로 플랫폼을 프로그래밍으로 다룰 수 있어야 하고 온디맨드여야 한다.

3장 인프라 정의 도구

인프라 정의 도구의 조건으로 스크립트로 사용할 수 있어야 하고 무인 실행이 가능해야 하며 구성이 외부에 노출되어야 한다는 점을 꼽고 이에 해당하는 현대의 도구들을 소개하고 있다.

4장 서버 구성 도구

인프라 정의 도구 외에 서버 구성 도구에 관해서 설명하고 있다. 여기에는 Ansible, Chef, Puppet 등이 포함된다. 모두 자동화를 위한 도구로 현재의 도구들의 특성을 정리해 놓았다.

5장 일반적인 인프라 서비스

인프라 서비스에서 필요한 내용을 정리하고 있다. 인프라에 공통으로 필요한 감시, 경보, 로깅 등의 관점을 설명하고 요즘 가상화된 특징 아래 서비스 탐색과 분산 프로세스 관리에 관해서도 얘기해서 구축해야 할 인프라의 전반적인 이해를 돕고 있다.

6장 서버를 프로비저닝하는 패턴

본격적으로 서버를 프로비저닝하는 여러 접근방법을 소개한다. 실제 도구는 아니고 패턴을 나누어서 서버의 스냅숏으로 서버를 띄운다거나 새 서버를 처음부터 프로비저닝하는 방법을 소개하고 있다.

7장 서버 템플릿을 관리하는 패턴

서버의 템플릿(AWS라면 AMI가 대표적)을 사용하는 다양한 패턴을 소개한다. 여기서는 템플릿으로 서버를 띄울 때 프로비저닝하거나 템플릿 자체를 프로비저닝 된 상태로 만들어두거나 두 가지를 섞어서 하는 접근 방법을 비교하고 이를 업데이트하는 등의 관리에서 발생할 수 있는 이슈도 소개한다.

중요한 모든 것을 템플릿에 프로비저닝한 후, 그 템플릿으로 서버를 생성하고 난 다음에는 런타임 데이터를 제외한 어떠한 변경도 허용하지 않는 것이 불변 서버의 핵심 개념이다.

8장 서버를 업데이트하고 변경하는 패턴

서버를 구축하고 나면 설정을 바꾸거나 새로운 버전을 적용하는 등 계속 업데이트해야 하는데 이 업데이트 일관성 있게 하기 위해 밀어 넣기/끌어오기 방식 혹은 마스터 없는 구성 등의 방법을 비교하고 업데이트할 때 조심해야 하는 부분 등을 설명한다.

9장 인프라를 정의하는 패턴

전체 인프라를 관리하는 방법을 소개하는 장이다. 직접 서버를 만들거나 환경별로 따로 정의해서 사용할 때의 문제점을 지적하고 인프라를 구조화해서 관리하기 쉽게 하려면 어떻게 해야 하는지 설명한다. 여기서는 애플리케이션 코드와 인프라 코드를 함께 관리하고 인프라 설계의 범위를 변경 범위를 기준으로 맞추라고 권장하고 있다.

기존 정적 인프라를 보유한 조직은 결국 정적 인프라의 아키텍처적 패턴과 구현에 익숙해지게 마련이다. 동적 인프라 플랫폼으로 넘어갈 때도 여전히 과거의 패턴과 구현을 유지하는 것이 의미 있고 필요하다고 생각하기 쉽다.

10장 인프라를 위한 소프트웨어 엔지니어링 관례

인프라 코드를 잘 관리하는 얘기인데 코드 얘기랑 같다. 품질 유지에 신경 써야 하고 VCS 사용하고 CI/CD를 적용하라고 하고 있다.

시스템을 코드로서의 인프라로 정의하고 도구를 사용해 구축한다고 해서 품질이 더 좋아지는 것은 아니다. 최악의 경우 상황이 더 복잡해질 수도 있다.

특정 변경이나 릴리스를 상용 환경에 실제로 투입할 시점을 결정하는 일은 여전히 필요하다. 이 결정은 사람이 하지만 실행은 도구가 한다.

11장 인프라 변경 시험하기

인프라를 정의했으면 코드와 마찬가지로 테스트하는 방법이다. 실제 테스트하는 예시나 구체적인 부분보다는 테스트 접근의 이론적인 부분이라 크게 도움 되진 않았다.

12장 인프라의 변경 관리 파이프라인

여기서 파이프라인은 인프라 적용을 자동화해서 코드가 올라가면 적용되는 자동화 흐름을 얘기한다. 이 장에서 한 설명은 나는 좀 과하게 느껴졌는데 파이프라인에는 어떤 단계가 있어 설계할 때의 주의점을 소개하고 실제 파이프라인의 여러 가지 패턴, 많은 팀에서 쓸 수 있는 패턴 등을 소개한다.

13장 인프라팀의 작업 흐름

인프라팀의 일하는 방식을 설명하는 장으로 모든 것을 자동화하고 로컬에서는 Vagrant 등으로 샌드박스 환경을 만들어서 사용하도록 권장하고 있고 이렇게 했을 때 얻는 장점 등을 설한다.

좋은 자동화 작업 흐름의 주요 특성은 속도와 신뢰성이다. 긴급 상황에 충분히 대처할 수 있을 정도로 변경을 빠르게 시스템에 적용할 수 있어야 한다.

코드로 인프라를 관리하는 일은 IT 운영자 대부분의 작업 방식과는 근본적으로 다르다. 가장 큰 변화는 서버와 인프라에서 직접 작업하던 것에서 벗어나 간접적인 방식으로 작업하게 된 것이다. 처음에는 좌절감을 줄 수 있다. 간단한 것을 더 느리고 더 복잡한 방식으로 하는 것처럼 느껴질 수 있기 때문이다.

인프라 코드를 효과적으로 작성하는 사람의 가장 중요한 습관은 가능한 모든 작업을 자동화하는 것이다.

14장 동적 인프라의 지속성

인프라에서 서비스를 중단없이 제공하기 위해 관심을 가지어야 할 지표와 무중단 배포를 하기 위해서 블루/그린 배포, 블루/그린인데 동적 인프라에서 필요할 때만 띄우는 피닉스 배포, 대량의 서버 중 일부에만 먼저 배포하는 카나리아 배포 방법을 소개한다.

사소한 문제와 '알려진 문제'를 감내할 수 있는 '충분히 좋은' 문화가 필요하다.

15장 코드로서의 인프라 준비하기

마지막으로 조직에서 Infrastructure as Code를 하려는 자세로 정리한다. 완벽한 설계가 아니라 계속 변경되는 진화적 아키텍처를 만들어야 하고 효과성을 측정하는 방법 등도 소개한다. 마지막으로 큰 조직에서 권한을 어떻게 나누는 게 좋고 안 좋은 점이 있는지를 설명한다.

댓글 쓰기

Viewing all 843 articles
Browse latest View live