Laravel with Jenkins (1/5) – Install Jenkins on DO using Terraform

이 글에서는, Laravel 프로젝트 build 파이프라인에 CI/CD 를 사용하기 위하여 Jenkins 를 DigitalOcean 에 설치하고, 더 나아가서는 Docker 를 사용한 전체적인 코드 배포 전략까지 설명한다. 비교적 간단한 Toy Project 의 인프라 설정 작업을 해가면서 정리한 내용이라서 use-case 에 따라서는 더 많은 부분을 고려해야하는 부분이 있겠으나, 다양한 CI/CD best practice 를 고민하는 팀에게 도움이 되었으면 좋겠다. 분량이 적지않아 여러 포스팅으로 나눠 5개 시리즈 포스팅으로 작성했다.

DigitalOcean

DigitalOcean 콘솔 Access Token 생성 페이지

첫번째 해야할 일은 테라폼을 실행하는 로컬 PC 에서 DO 인프라와 인터액션을 하기위한 access 토큰 생성이다. 위 스크린샷에 보이는 것 처럼 DO 콘솔 좌측하단의 API 메뉴를 선택하고 Personal access tokens 의 Generate New Token 버튼을 클릭하면, access 토큰을 생성할 수 있다. 이 토큰은 인프라 리소스를 생성할 수 있어야만 하므로, read/write 권한을 모두 선택한다.

그 다음에 해야할 일은 테라폼으로 생성한 DO 인스턴스와 로컬 PC 간의 ssh 통신을 할 수 있도록 로컬 PC 의 ssh 키를 DO 에 등록하는 과정이다. SysOp 의 로컬 PC 에 이미 ssh 키가 등록되어 있단 가정 하에 아래 명령으로 public ssh 키를 복사한다. (이 ssh 키에는 패스워드가 없어야 하며, 필요하다면 디폴트 ssh 키가 아닌 새로운 ssh 키를 생성하여, 테라폼에서 DO 인스턴스를 생성할 때만 해당키를 지정하여 사용하는 방법도 가능하다.)

cat id_rsa.pub | pbcopy
DigitalOcean 콘솔 SSH key 등록 페이지

위 스크린샷에서 볼 수 있는 것 같이 SSH keys 등록 페이지를 통하여, 로컬 PC 의 SSH 키를 붙여넣고 원하는 이름으로 등록 후 해당키의 fingerprint 를 기록해 둔다. 이는 이후 작업에서, fingerprint 로 지정한 SSH 키를 (또는 복수의 SSH 키들을) 프로비져닝할 인스턴스에 자동으로 등록하기 위함이다.

이제 PC 에 테라폼을 설치한다. 맥 사용자의 경우 homebrew install 명령으로 간단한 설치가 가능하다.

Terraform

https://www.terraform.io/docs/providers/do/index.html

테라폼 문서의 DO 섹션을 보면 테라폼으로 프로비져닝할 수 있는 DO 리소스들이 리스팅 되어 있다. 우리가 우선 사용할 리소스는 digitalocean_droplet 과 digitalocean_record 등 이다.

Provisioning Nginx server using Terraform

이제 로컬 PC 의 적당한 위치에 테라폼 코드를 저장할 폴더를 생성하고 아래와 같은 내용을 갖는 provider.tf 파일을 만든다.

variable "do_token" {}
variable "pub_key" {}
variable "pvt_key" {}
variable "ssh_fingerprint" {}

provider "digitalocean" {
  token = "${var.do_token}"
}

다음으론, 간단한 nginx 서버를 DO 에 프로비져닝하기 위한 nginx.tf 이란 파일을 만들어보자.

resource "digitalocean_droplet" "nginx" {
  image              = "ubuntu-18-04-x64"
  name               = "nginx"
  region             = "sgp1"
  size               = "s-1vcpu-1gb"
  private_networking = true
  monitoring         = true
  ssh_keys = [
    "${var.ssh_fingerprint}",
  ]
  connection {
    user        = "root"
    type        = "ssh"
    private_key = "${file(var.pvt_key)}"
    timeout     = "2m"
  }
  provisioner "remote-exec" {
    inline = [
      "export PATH=$PATH:/usr/bin",
      # install nginx
      "sudo apt-get update",
      "sudo apt-get -y install nginx",
    ]
  }
}

이제까지의 준비로, 테라폼 실행에 필요한 파일들이 모두 준비되었다. 처음 내릴 테라폼 명령은 terraform init 이다. 이 명령은 필요한 플러그인들을 다운받고 초기화한다.

terraform init

초기화 후에는 이번 프로비젼 단계의 인프라 변경내용을 확인하기 위하여, terraform plan 명령을 실행한다. (DO 에서 생성한 access token 값과 ssh fingerprint 값은 각각 DO_TOKEN 과 SSH_FINGERPRINT 환경변수로 미리 export 했다고 가정한다. 또한, 디폴트 id_rsa 가 아닌 별도의 ssh 키를 DO 연결을 위해 생성하였다면 해당 파일 이름으로 변경해야한다.)

terraform plan \
-var "do_token=${DO_TOKEN}" \
-var "pub_key=$HOME/.ssh/id_rsa.pub" \
-var "pvt_key=$HOME/.ssh/id_rsa" \
-var "ssh_fingerprint=$SSH_FINGERPRINT"

Terraform 으로 실제 인프라를 프로비젼하기 위해서는, terraform apply 명령을 실행하면 되며, 약 1분 정도의 시간이 경과된 후에는 새로 생성된 DO droplet 에 nginx 가 설치된 것을 확인 할 수 있다.

terraform apply \
-var "do_token=${DO_TOKEN}" \
-var "pub_key=$HOME/.ssh/id_rsa.pub" \
-var "pvt_key=$HOME/.ssh/id_rsa" \
-var "ssh_fingerprint=$SSH_FINGERPRINT"

생성된 리소스의 정보를 확인하려면 terraform show명령으로 사용한다. 생성된 리소스를 제거하기 위해서는 terraform plan -destroyterraform apply 명령을 순차적으로 실행하던지, 아래와 같이 terraform destroy 명령을 실행하면 된다.

terraform destroy \
  -var "do_token=${DO_TOKEN}" \
  -var "pub_key=$HOME/.ssh/id_rsa.pub" \
  -var "pvt_key=$HOME/.ssh/id_rsa" \
  -var "ssh_fingerprint=$SSH_FINGERPRINT"

만일 nginx 서버 인스턴스 생성시 문제가 발생하였다면, 커맨드라인에서, export TF_LOG=1 이라고 설정한 뒤 나오는 로그 기록을 살펴보면서, trouble-shoot 을 할 수 있다.

테라폼 실행시 환경변수를 인자로 주는 것이 불편하다면, 민감한 credentials 정보를 별도의 파일로 만들고 .gitignore 에 등록하여 그 내용을 보호할 수도 있다. (이 방법의 예는 Dockerized Laravel PHP 앱 인스턴스를 만들때 실제 코드로 살펴볼 예정이다.) 일단, nginx 프로비져닝에 성공 했다면 해당 인스턴스를 제거하고, 이 포스팅에서 목표로 하는 젠킨스 인스턴스를 만들어 보자.

Provisioning Jenkins using Terraform

Jenkins 인스턴스를 만들기 위해, jenkins.tf 라는 파일을 아래와 같이 만든다.

resource "digitalocean_droplet" "jenkins" {
  image              = "ubuntu-18-04-x64"
  name               = "jenkins"
  region             = "sgp1"
  size               = "s-1vcpu-1gb"
  private_networking = true
  monitoring         = true
  user_data          = "${file("config/jenkins-userdata.sh")}"

  ssh_keys = [
    "${var.ssh_fingerprint}",
  ]

  connection {
    user        = "root"
    type        = "ssh"
    private_key = "${file(var.pvt_key)}"
    timeout     = "2m"
  }
}

앞서 보인 nginx.tf 와 비교하면, provisioner “remote-exec” 블럭이 없어지고, user_data 로 jenkins-userdata.sh 라는 이름의 bash 스크립트 파일을 아래와 같이 만들어서 프로그램 설치 및 설정을 실행하도록 했다.

일반적으로는 Terraform/Ansible 조합을 선호하는데, 이후 Dockerized Laravel PHP 앱 인스턴스를 만들때 해당 조합으로 프로비져닝을 할 예정이므로, 비교해 보면 좋겠다.

#!/bin/bash

export DEBIAN_FRONTEND=noninteractive
export PATH=$PATH:/user/bin
export LANG=en_US.UTF-8
export LANGUAGE=en_US:en
export LC_ALL=en_US.UTF-8

# create ssh key to communicate b/w jenkins and repository
ssh-keygen -t rsa -N "" -f ~/.ssh/id_rsa

# add sudoable jenkins user
adduser jenkins
usermod -aG sudo jenkins
rsync --archive --chown=jenkins:jenkins ~/.ssh /home/jenkins

# add additional apt repositories
wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ | sudo tee -a /etc/apt/sources.list.d/jenkins.list'
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
add-apt-repository -y "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
add-apt-repository -y ppa:certbot/certbot

# pull the packages for installing jenkins
apt update && apt -y upgrade

# add 2gb swap memory
fallocate -l 2G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
echo 'vm.swappiness=10' | sudo tee -a /etc/sysctl.conf
echo 'vm.vfs_cache_pressure=50' | sudo tee -a /etc/sysctl.conf

# setting up a basic firewall
ufw allow 22
ufw allow 80
ufw allow 443
ufw allow 8080
ufw --force enable

# install general tools
apt install -y apt-transport-https ca-certificates
apt install -y software-properties-common python-software-properties build-essential
apt install -y vim git curl htop

# install java
apt install -y openjdk-8-jdk

# install jenkins
apt install -y jenkins

# install docker and docker-compose
apt install -y docker-ce
apt install -y docker-compose

# install nginx
apt install -y nginx

# install certbot
apt install -y certbot python-certbot-nginx

# install s3cmd (for easy dot-env file management)
apt install -y s3cmd

# remove packages no longer needed
apt remove -y --purge software-properties-common
apt -y autoremove

# add jenkins to docker group
sudo usermod -aG docker jenkins

# download nginx config
wget https://gist.githubusercontent.com/jinseokoh/cf21d257fda1d858e298c7322e8b2c5b/raw/60d98dde66133c3cadea53ee4205f740f8438238/nginx.conf -O /etc/nginx/sites-available/default

# let certbot to update nginx config
certbot --nginx --non-interactive --redirect --agree-tos -m "[email protected]" -d jenkins.example.com

# restart services
systemctl start jenkins
systemctl reload nginx

# print out initial password
cat /var/lib/jenkins/secrets/initialAdminPassword

위의 스크립트는 테라폼으로 생성한 Jenkins 인스턴스에 필요한 패키지들을 설치한다. 주요 작업 리스트는 아래와 같다.

  • ssh 키 생성
  • jenkins 사용자 생성
  • 2GB swap 메모리 설정 (1G 메모리 밖에 없는 저렴한 인스턴스라…)
  • java, jenkins 설치
  • docker, docker-compose 설치
  • nginx, certbot 설치
  • s3cmd 설치 (Laravel PHP 앱의 dot-env 파일 복사용)
  • nginx reverse proxy 설정

참고로, nginx reverse proxy 설정은 아래의 내용과 같다.

server {
  listen 80 default_server;
  server_name localhost;

  location / {
    proxy_pass            http://localhost:8080;
    proxy_set_header      Host $host:$server_port;
    proxy_set_header      X-Real-IP $remote_addr;
    proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header      X-Forwarded-Proto $scheme;
    proxy_connect_timeout 120;
    proxy_send_timeout    100;
    proxy_buffers         4 32k;
    client_max_body_size  8m;
    client_body_buffer_size 128k;

    # Required for new HTTP-based CLI
    proxy_http_version 1.1;
    proxy_request_buffering off;
    proxy_buffering off; # Required for HTTP-based CLI to work over SSL
    # workaround for https://issues.jenkins-ci.org/browse/JENKINS-45651
    add_header 'X-SSH-Endpoint' 'jenkins.example.com:50022' always;
  }
}

참고로, Jenkins 가 도커 이미지 빌드시 Laravel 코드베이스에서 사용할 .env 파일을 DO Spaces (AWS s3 의 alternative) 에 미리 업로드해 놓고 복사하여 사용할 예정이라서 이 인스턴스에 s3cmd 를 설치했고, s3cmd 의 configuration 은 아래 문서를 참고하여 수동으로 설정했다.

https://www.digitalocean.com/docs/spaces/resources/s3cmd/

Jenkins 인스턴스에서, git 리파지토리로부터 소스코드를 읽어올 수 있도록 GitHub (또는 bitbucket) 의 설정페이지로 이동 후, Jenkins 인스턴스 .ssh 폴더의 id_rsa.pub 키를 등록한다.

해당 인스턴스가 생성된 이후, DO 의 DNS 에 jenkins.example.com 라는 이름의 A 레코드를 등록하는 테라폼 코드를 아래와 같이 추가한다. (참고로, 스크립트 및 설명 전반에 노출되는 도메인 이름은 example.com 로 명명하겠다.)

resource "digitalocean_record" "jenkins" {
  domain = "example.com"
  type   = "A"
  name   = "jenkins"
  value  = "${digitalocean_droplet.jenkins.ipv4_address}"
}

아래 GitHub 리포에 방문하면, 테라폼 관련 코드를 볼 수 있다.

https://github.com/jinseokoh/terraform

이로서, 단 월 $5 의 비용으로 유지/관리할 수 있는 Jenkins 인스턴스를 설치해 보았다. 다음 포스팅에서는 Laravel 프로젝트에서 사용하기 위한 기본적인 Jenkins 설정 내용을 살펴보도록 하겠다.

Leave a Reply