이 글은 2018년 10월 Github Universe에서 공개된 Actions라는 기능으로 개인서버에서 돌리고있던 Cronjob을 대체해보는 과정을 설명한 글입니다.
2018년 10월, 매년 열리는 Github Universe에서 Actions라는 새로운 기능을 소개했습니다.
Github Actions는 Github 서비스의 특정 이벤트에 자동으로 작동하는 컨테이너 기반 워크플로우 자동화 서비스입니다. 2019년 5월 현재까지는 아직 public beta 기간이며, early access 등록은 여기에서 할 수 있습니다.
Github Actions에 대한 자세한 설명과 튜토리얼은 공식문서(영문)와 Outsider님의 블로그에 잘 정리가 되어있습니다.
기존 Actions는 push, pull request, fork, issue, watch, status 등 총 27가지의 이벤트를 지원했었는데, 얼마전 2019년 4월 5일, 특정한 이벤트 발생없이 cron 문법으로 일정 시간마다 주기적으로 워크플로우를 실행할 수 있는 scheduled 이벤트가 추가되어 총 28가지의 이벤트를 지원하게 되었습니다.
이 업데이트를 보고, 이 기능을 이용해서 개인 목적으로 EC2 인스턴스에서 돌리고있는 cronjob들을 대체할 수 있겠다고 생각했습니다. 다른 CI 서비스에도 cronjob 기능이 있고, AWS Lambda같은 serverless에 올려서 주기적으로 request를 날릴 수도 있지만, Actions public beta 도 승인받았고, 뭔가 새로나온 기능을 더 사용해보고 싶었습니다.
아래의 모든 예제는 이 저장소에 공개되어있습니다.
저는 가끔 심심하면 집앞 영화관에서 영화를 자주 보는데요. 매번 갑자기 영화가 보고싶을 때마다 네이버에 "영화"를 검색하고 "현재상영영화"와 "개봉예정영화"를 확인했었습니다.
이렇게 자주 확인하다보니, 이를 Github Actions로 자동화해서 "매일 아침 8시에 현재상영영화와 개봉예정영화 리스트를 메일로 보내주는 간단한 서비스"를 만들어보자 생각했습니다.
먼저 puppeteer와 nodemailer로 "현재상영영화"와 "개봉예정영화" 스크린샷을 찍어서 제 이메일로 보내는 간단한 node 스크립트를 아래와 같이 작성했습니다.
// index.js
const puppeteer = require('puppeteer')
const nodemailer = require('nodemailer')
require('dotenv').config()
const CURRENT_MOVIES_URL =
'https://search.naver.com/search.naver?where=nexearch&sm=tab_etc&query=%ED%98%84%EC%9E%AC%EC%83%81%EC%98%81%EC%98%81%ED%99%94&mra=bkEw'
const FUTURE_MOVIES_URL =
'https://search.naver.com/search.naver?where=nexearch&sm=tab_etc&query=%EA%B0%9C%EB%B4%89%EC%98%88%EC%A0%95%EC%98%81%ED%99%94&mra=bkEw'
const now = Date.now()
const takeScreenshots = async _ => {
const browser = await puppeteer.launch({ args: ['--no-sandbox'], executablePath: 'google-chrome-unstable' })
const page = await browser.newPage()
await page.goto(CURRENT_MOVIES_URL)
const clip = { x: 0, y: 240, width: 660, height: 540 }
await page.screenshot({
path: `now-${now}.png`,
clip,
})
await page.goto(FUTURE_MOVIES_URL)
await page.screenshot({
path: `future-${now}.png`,
clip,
})
await browser.close()
}
const sendEmail = async _ => {
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: process.env.GMAIL_ID,
pass: process.env.GMAIL_PW,
},
})
const to = [process.env.GMAIL_ID]
const mailOptions = {
from: process.env.GMAIL_ID,
to,
subject: '현재상영영화 & 개봉예정영화',
html: `<img src="cid:now-${now}"/><br><img src="cid:future-${now}"/>`,
attachments: [
{
filename: `now-${now}.png`,
path: `./now-${now}.png`,
cid: `now-${now}`,
},
{
filename: `future-${now}.png`,
path: `./future-${now}.png`,
cid: `future-${now}`,
},
],
}
const info = await transporter.sendMail(mailOptions).catch(e => console.log(e))
console.log('Email sent: ' + info.response)
}
!(async _ => {
await takeScreenshots()
await sendEmail()
})()
그리고 스크립트를 실행해보면,
$ node index.js
다음과 같은 메일이 오는 것을 확인할 수 있습니다.
다음은, Actions는 다른 CI/CD 서비스와 마찬가지로 컨테이너 기반 워크플로우 서비스이기 때문에 어떤 런타임에서 실행할 것인지에 대한 컨테이너를 정해주어야 합니다.
Github Actions 공식 organization에 들어가보면 다양한 예제들을 제공하고 있는데, 역시 가장 큰 node ecosystem인 만큼 npm action도 예제로 제공하고있습니다.
이 경우에도 간단한 node 스크립트이기 때문에 제공하는 npm action을 사용할 수도 있지만, linux 환경에서 puppeteer를 사용하려면 추가적인 패키지 설치가 필요합니다. (관련이슈)
이 때문에, 직접 Custom workflow로 puppeteer의 공식 Docker Guide를 따라 다음과 같은 Dockerfile을 작성했습니다.
FROM node:10-slim
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \
&& sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \
&& apt-get update \
&& apt-get install -y google-chrome-unstable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
RUN apt-get -y update
RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
COPY . .
RUN npm i
CMD npm run send
이제 이 컨테이너를 매일 아침 8시에 실행하도록 workflow를 설정해야합니다.
Github Actions가 활성화된 저장소라면, 상단에 위과 같이 Actions 탭이 활성화된 것을 볼 수 있습니다.
가운데 Create a new workflow
버튼을 누릅니다.
다시 파란글씨의 Create a new workflow
를 누르면,
위와 같은 Workflow visual editor가 나타납니다.
Workflow 이름을 정하고 Run event에서 새로 추가된 scheduled를 선택합니다.
오른쪽에는 cron 문법과 동일한 문법으로 실행 주기를 설정할 수 있습니다.
주의할 점: linux의 crontab은 시스템 시간을 기준으로 동작하지만, Github Actions의 scheduled 이벤트는 UTC를 기준으로 합니다.
따라서 매일 오전 8시에 실행하기 위해서는 위와 같이 한국시간 기준으로 9시간을 뺀 오후 11시(0 23 * * *)로 작성해야합니다.
실행주기를 정하고, 파란점을 드래그해서 아래로 놓으면,
다음과 같이 첫번째 액션을 선택할 수 있습니다.
액션은 실행할 Docker image입니다. 메뉴에서 선택하거나 아래와 같이 다양한 형태로 작성할 수 있습니다.
방금 전 실행할 Dockerfile을 만들었으니 그 파일의 경로를 적어줍니다.
제 경우엔 Dockerfile을 저장소 root에 두었기 때문에 표에서 세번째 방법으로 ./
만 입력합니다.
Action의 이름을 정하고, 환경변수(nodemailer에서 gmail 인증을 하기 위한 id와 password)를 설정해준 뒤 커밋합니다.
그러면 저장소 root에 .github
폴더 안에 다음과 같이 HCL로 작성된 main.workflow
파일이 자동으로 생성됩니다.
workflow "Run every 8 AM" {
on = "schedule(0 23 * * *)"
resolves = [
"Take screenshots & Send Email",
]
}
action "Take screenshots & Send Email" {
uses = "./"
env = {
GMAIL_ID = "ysm0622@gmail.com"
}
secrets = ["GMAIL_PW"]
}
마지막으로 workflow 생성은 끝났지만, 이 workflow가 잘 동작하는지 확인하려면 오전 8시가 될 때까지 기다려야합니다.
이러면 당연히 비효율적이기 때문에, act라는 툴로 이 action을 local 환경에서 실행시켜 볼 수 있습니다. (설치는 저장소 참고)
act는 2019년 5월 22일 현재 아직 schedule 이벤트를 지원하지 않기 때문에(관련이슈) local에서 잠시 on event를 push로 변경한 후 act
명령어를 사용하면 정상적으로 이메일이 도착하는 것을 확인할 수 있습니다.
이 글에서는 Github Actions에 새롭게 추가된 scheduled workflow 기능으로 crontab을 만들어보았습니다.
이 기능이 추가되고 난 뒤, 온라인 crontab 서비스를 몇가지 찾아봤지만 다음과 같이 모두 유료이거나, 무료더라도 제약이 많았습니다.
물론 managed service라는 점에서 비교가 어렵지만, 새롭게 추가된 action은 fully customize가능하고, Github의 서비스라는 점, (아직까진)무료라는 점에서 충분히 매력적입니다.
CI/CD 뿐만아니라 크롤링, 봇 시간알림 등에 앞으로 유용하게 사용할 수 있을 것 같습니다.
긴 글 읽어주셔서 감사합니다.
2019/5/22: Github Actions public beta 신청하고 아직 승인되지 않았는데, Actions를 빨리 사용해보고 싶으신분은 제 이메일이나 댓글로 메일주소를 남겨주세요. Actions가 활성화된 organization에 초대해 드리겠습니다. 😁
위 영화 메일링도 받고싶으신 분은 제 이메일이나 댓글로 메일주소를 남겨주시거나, 여기에 이메일을 추가해서 Pull Request 날려주세요.
오오 이거 크론잡에도 되는군요! 저도 초대 될까요?
gej.okpo@gmail.com
헤로쿠 시간을 다써서ㅠㅠ 죽어있는 봇들을 살려야겠어요