Realtime broadcasting with Laravel and NuxtJS (1/4)

Prologue

Laravel 은 웹소켓을 사용한 실시간 브로드캐스트 기능을 “out of the box” 로 지원한다. 이를 위해 Laravel Echo (https://github.com/laravel/echo) 라 하는 클라이언트 라이브러리가 프론트앤드 패키지의 일부로 제공되는데, 이를 Nuxt 프로젝트에서 사용할 수 있도록 만든 wrapper 모듈인 @nuxtjs/laravel-echo (https://github.com/nuxt-community/laravel-echo) 를 이용하면 Nuxt 프로젝트에서도 쉽게 리얼타임 기능을 사용할 수 있다.

공식 문서에 언급되어 있듯, Laravel 에서 기본으로 제공하는 브로드캐스트 드라이버는 Pusher Channel 과 Redis 가 있는데, Pusher Channel 의 경우 트래픽이 많은 프로덕션 환경에서 사용하기는 비용적인 문제가 있어 가성비를 경쟁력으로 삼아야 하는 스타트업 인 경우, 썩 좋은 옵션으로 보이지 않았다. 더우기 채팅기능을 위해 Pusher Chatkit 까지 사용해야 한다면, 가격 부담이 더욱 커지게 느껴질 것이다.

Pusher (https://pusher.com) 의 다른 대안으로는 Pusher API 를 PHP Ratchet (http://socketo.me) 을 이용하여 개발한 Laravel WebSockets (https://docs.beyondco.de/laravel-websockets) 을 사용하는 방법이 있다. 이 패키지를 사용하여 웹소켓 서버 박스를 만든 다음, Laravel 의 Pusher 브로드캐스트 드라이버가, Pusher 서버가 아닌, 커스텀 짝퉁 pusher 박스를 이용하도록 만드는 방법이다. (step by step 설명은 아래 유튜브 강의 참고.)

하지만, 이 또한 트래픽이 많아져서 해당 서비스의 horizontal 스케일이 필요할 때까지 고려한다면, 프로덕션용으로 viable path 가 될 지 걱정스런 부분이 있다. horizontal 스케일을 위해, ELB 아래에 여러 서비스 인스턴스를 연결하게 된다면, 결국 peer 인스턴스들 간에 데이터 동기화를 위한 로직이 필요할텐데, 이러한 커스터마이징을 위하여, PHP Ratchet 을 사용하는 방법이 Node.js Express 를 사용하는 방법 보다 더 나은 path 인지 여러 이유에서 확신이 서지 않았다.

또 다른 대안으로는 firebase 를 사용하는 방법인데, 옆 사무실에서는 잘 사용하고 있다지만, 이 역시 트래픽 증가를 고민할 단계 쯤 되면 viable 한 솔루션이 되지 않는다는 부정적인 견해가 팽배하여, 기술적인 부분을 깊게 알아보지 않고 쉽게 포기해 버렸다. 물론, 어떠한 문제들도 영리한 work around 방법들이 존재하겠지만, 현재 내가 갖고 있는 리소스들을 고려해봤을때 Laravel Echo Server (https://github.com/tlaverdure/laravel-echo-server) 라는 오픈소스 프로젝트를 근간으로 개발하는 것이 가장 현명한 방법이 되리라 판단했다.

서두가 길었는데, 결국 위에서 언급한 이유들로 인하여, AWS 환경에서 ALB 와 AutoScaling 기능을 이용하여 horizontal 스케일이 가능한 Redis 와 NoSQL (MongoDB 나 DynamoDB 중 하나) 을 사용하는 Laravel Echo 호환 node.js 서버의 개발하려고 한다. 다만, 이를 위한 아래에 열거된 pre-requisite 들이 Laravel/Nuxt 개발자인 나에게 만만치 않아 보이지만 말이다.

  • node.js Express 학습
  • TypeScript 학습
  • Laravel Echo Server 프로젝트 소스 분석
  • Chat 용 인증 메카니즘
  • Redis pub/sub 용 데이터 분석/설계
  • RDB/MongoDB 용 스키마 설계
  • horizontal 스케일 가능하도록 부분 변경
  • 테스트/배포

다행인 것은 비슷한 주제의 다양한 레퍼런스 자료들은 즐비하다는 것.

Learning new things

Node.js 학습은 일단 Udemy 강좌 중 Node.js, Express, MongoDB & more: The Complete Bootcamp 를 선택해서 들었는데, 내용이 42시간이 넘는 정도의 분량이라, 짧은 시간내에 모두 듣기엔 좀 인내심이 필요하다. 하루 8시간 씩 듣는다 쳐도 6일 분량. 모노토너스한 목소리 톤 때문에 깜박 졸았다면, 어흑… 섹션 9까지 듣고, 아래 섹션들은 일단 패스한다. 시간있을 때 다시!

https://www.udemy.com/course/nodejs-express-mongodb-bootcamp

  • 섹션 10. 인증
  • 섹션 11. 몽구스 모델링
  • 섹션 12. pug 템플릿 엔진
  • 섹션 13. 고급 기능
  • 섹션 14. 디플로이먼트

TypeScript 는 Frontend Masters 의 강좌인 TypeScript 3 Fundamentals, v2 를 선택해서 들었다.

https://frontendmasters.com/courses/typescript-v2/

이 강의는 4시간 분량으로 오전 작업 후 남은 하루 일정에 도전해 볼 만한 분량이다. TS 의 주된 특징들을 설명하면서 넘어가는 것까지는 좋았는데, 강의 내용을 기초로 실제 프로젝트를 시작하기 부족함이 느껴져서 다른 강의를 하나 더 듣게 되었다. 아래의 Pluralsight 기초 TypeScript 강좌. 이러다가 조낸 강의만 듣다가 에너지를 소진하고 끝내버릴 분위기…

https://www.pluralsight.com/courses/typescript-projects-configuring-compiling-debugging

Time to get my hands dirty

늘어진 스케쥴로 인해 제대로 된 path 를 가고 있는지 확신이 줄어들긴 했지만, 일단 VueJS 를 위한 JS 코딩 스타일에 맞춰진 나의 Visual Studio Code 의 ESLint 와 Prettier 옵션부터 TypeScript 를 위해 수정했다. .ts 파일 저장도 제대로 안되면 안되므로. 어쨌든, Visual Studio Code 의 settings.json 은 아래와 같이 변경하여, TypeScript 를 대응하도록 했다.

{
  "": {
    "editor.formatOnSave": false
  },
  "": {
    "editor.formatOnSave": false
  },
  "[javascriptreact]": {
    "editor.formatOnSave": false
  },
  "[typescript]": {
    "editor.formatOnSave": false
  },
  "[typescriptreact]": {
    "editor.formatOnSave": false
  },
  "": {
    "editor.formatOnSave": false
  },
  "diffEditor.renderSideBySide": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.formatOnSave": true,
  "editor.formatOnPaste": true,
  "editor.largeFileOptimizations": false,
  "editor.minimap.enabled": false,
  "editor.tabSize": 2,
  "eslint.alwaysShowStatus": true,
  "eslint.validate": [
    "vue",
    "html",
    "javascript",
    "typescript",
    "typescriptreact"
  ],
  "explorer.confirmDelete": false,
  "explorer.openEditors.visible": 0,
  "files.associations": {
    "*.module": "php"
  },
  "files.maxMemoryForLargeFilesMB": 20480,
  "php.suggest.basic": false,
  "phpcs.enable": false,
  "prettier.vueIndentScriptAndStyle": true,
  "sync.gist": "e38a9bd8b19391bb6ce89814e8e621f1",
  "terminal.integrated.fontFamily": "D2Coding",
  "terminal.integrated.fontSize": 13,
  "terminal.integrated.shell.osx": "/bin/zsh",
  "vetur.completion.useScaffoldSnippets": false,
  "vetur.validation.template": false,
  "window.zoomLevel": 0,
  "workbench.colorTheme": "Monokai",
  "workbench.iconTheme": "vscode-icons",
  "editor.detectIndentation": false,
  "html.format.preserveNewLines": false,
  "eslint.probe": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue"
  ]
}

Laradock to the rescue

다음 편에서는 로컬환경에서 Laradock 을 사용하여, 프로덕션과 비슷한 환경으로Frontend, Backend 및 Laravel Echo Server 를 설정해 보겠다.

Posted by admin in Backend, 0 comments

It's been a while to have something dockerized and deploy it in a production ready environment. To shape up my DevOp skills, I will try to set up a new ECS environment and blog what I did along the way. The application I am working on is built on top of the latest Nuxt JS. So, If you happened to follow something similar path, hope you find this somewhat helpful to better understand an option that I came up with.

Things that I have on my mind

  1. Build a front-end universal app using the latest NuxtJS and Vuetify and some other goodies I can think of.
  2. Build a back-end API using the latest Laravel 6
  3. Deploy the front-end to AWS ECS/Fargate
  4. Deploy the back-end to AWS EB

What I'd like to address this time is #3. The rest of the list have been addressed already and I've got solid work experience on the topics. When you have your own Docker container ready to use, you can deploy it to AWS ECS within a short period of time. you can even deploy it to AWS Elastic Beanstalk. But, ECS/Fargate route is what I've chosen to go with this time. To have a basic setup, read the article below along. Its step by step explanation is easy to follow.

https://itnext.io/run-your-containers-on-aws-fargate-c2d4f6a47fda

As with many other things in IT, however, there're a lot more to come. This one is no exception. Take a look at the following article written specifically on how to deploy NuxtJS app to ECS. The writer mentioned about runtime environment variable injection for the sake of security.

https://melvinkoh.me/dockerizing-and-deploying-nuxtjs-ssr-apps-to-aws-ecs-ck1egcwoi00dmjfs1w2cr3shq

With my current project, I've set up the following 5 variables in AWS System Manager Parameter Store. As you can see I added a prefix (prod-) to all of 'em.

So that I can easily write a rule to have an access right in the following fashion.

https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html

In case you wonder, my IAM ecs-task-execution-role has an inline policy like:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:DescribeParameters"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters"
            ],
            "Resource": [
                "arn:aws:ssm:ap-northeast-2:123456000000:parameter/prod-*"
            ]
        }
    ]
}

But then, I confronted unexpected runtime environment variable issue with NuxtJS. A funny thing is that I've got no issues whatsoever on my local PC. But, NuxtJS didn't read the run-time environment variables in the production environment. WTF?! After further investigation,

https://github.com/nuxt/nuxt.js/issues/5100

I found the issue thread in its GitHub repo. As a workaround I was able to get around the problem by using samtgarson/nuxt-env and adding additional logic to my code base. Now, I can inject run-time environment variables with the package. As described in several posts in the thread, either ctx.app.$env.XXX or this.$env.XXX works depending on the usage.

Dockerizing Nuxt SSR app and deploy it to AWS ECS/Fargate
Posted by admin in Backend, 0 comments

Deploy Laravel 6 app to AWS Elastic Beanstalk

Mac 에 PHP 7.3 설치

사용하는 Mac 의 PHP 버젼이 behind 7.3 였기에, 다음 링크를 참고하여, Homebrew 로 7.3 을 설치했다.

https://stitcher.io/blog/php-73-upgrade-mac

아래 PATH 를 .zshrc 에 추가하고, php -v 했을때 7.3.12 가 출력되는 것을 확인했다.

  echo 'export PATH="/usr/local/opt/[email protected]/bin:$PATH"' >> ~/.zshrc
  echo 'export PATH="/usr/local/opt/[email protected]/sbin:$PATH"' >> ~/.zshrc

Mac 에 Python 3.7 설치

내친김에 Python 도 업그레이드. 아래 PATH 를 .zshrc 에 추가하고, 버젼을 확인했다.

export PATH="/usr/local/opt/python/libexec/bin:$PATH"

Mac 에 EB-CLI 설치

EB CLI 가 없는 PC 였기에, 다음 링크를 참고하여, Homebrew 로 CLI 를 설치했다.

https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/eb-cli3-install-osx.html

Mac 에 AWS-CLI 설치

brew install awscli 명령으로 설치했다. IAM 사용자 생성 후, configuration 해야만 된다.

RDB 생성

Poorman’s DB Client 인 Sequel Pro 를 사용하는 탓에 Free Tier 에 맞는 template 을 사용하여, RDB 를 생성시 MySQL 5.7 버젼을 선택했다. Public accessibility 은 YES 로 설정하여, Mac 에서 접속이 가능토록 했다. (지금까지의 설정으로 외부 접속이 가능한듯 싶었으나, 인바운드 3306 포트를 연 커스텀 VPC security group 을 추가하지 않으면 접속이 불가능 했다. default security group 의 인바운드/아웃바운드 규칙을 보면 wide-open 으로 되어있기 때문에, 접속이 불가능한 이유가 명쾌하게 이해되지 않지만, anyway 새로운 인프라마다 필요한 security group 을 추가해야 한다고 경험상 알고 있다. 가령 redis 는 redis_sg 를 추가하고 rdb 는 rdb_sg 를 추가한다.)

Elastic Beanstalk 앱 생성

Web Server 환경으로 콘솔의 요구사항을 입력하여, High availability 설정 preset 을 선택하면 Application Load Balancer 를 포함하는 PHP v7.3 앱 환경을 생성할 수 있는데, 이 설정은 production 이라는 이름으로 생성했다. 다른 모든 옵션은 default 로 설정했다. 로드 밸런서가 만들어지면, Route53 과 Aliased A 레코드를 넣어서 연결하면 된다. ACM (Amazon Certificate Manager) 으로 설정하면 https 에 필요한 certificate 에 필요한 CNAME 레코드 생성과 자동갱신등이 지원되므로 관리적인 측면에서 상당히 편리하다.

EB CLI 설치 및 IAM 메뉴에서 사용자 생성

eb init 을 이용하여, 최초 초기화를 하려 했을때, credentials 을 설정하지 않았다는 에러메시지가 나왔다. IAM 메뉴로 이동할 차례이다. Add user 메뉴에서 관리자 계정을 생성한다. 이를 통하여, Access key ID 와 Secret access key 를 받는다. 이는 eb CLI 의 aws-access-id 와 aws-secret-key 로 대응된다. (이런 이름의 불일치는 attention to detail competency 가 떨어져 보이지만, anyway.) 이제, 코드 베이스에 .elasticbeanstalk 폴더가 생성된 것이 보인다.

EB 가 생성한 EC2 는 S3 와 SQS 에 대한 access 권한이 있어야 하는데, 이는 IAM 메뉴의 Roles 항목에서 aws-elasticbeanstalk-ec2-role 에 추가했다.

AWS Console 에서 production 환경 변경

production > configuration 에 container option 을 보면, Document root 옵션이 있는데, 이를 /public 으로 수정했다.

ElastiCache Redis 설정

PHP 의 Redis 라이브러리인 predis/predis 가 버려진 프로젝트가 되었기에, EB 환경에 php-redis/php-redis 를 소스 설치를 위한 아래의 설정을 추가했다. Redis 설정은 간단하므로 생략한다. 다만, redis-sg 를 만들어서, 인바운드 port 를 열어두는 걸 잊지 말자. 아, 그리고 Laravel config/app.php 파일의 facade alias 인 Redis 를 클래스 충돌을 방지하기 위해 RedisManager 로 변경하라는 매뉴얼의 조언도 따랐다.

# these commands run before the application and web server are
# set up and the application version file is extracted.
commands:
    01_redis_install:
        # run this command from /tmp directory
        cwd: /tmp
        # don't run the command if phpredis is already installed (file /etc/php.d/redis.ini exists)
        test: '[ ! -f /etc/php.d/redis.ini ] && echo "phpredis extension not installed"'
        # executed only if test command succeeds
        command: |
            wget https://github.com/phpredis/phpredis/zipball/master -O phpredis.zip \
            && unzip -o phpredis.zip \
            && cd phpredis-* \
            && phpize \
            && ./configure \
            && make \
            && make install \
            && echo extension=redis.so > /etc/php.d/redis.ini

AWS s3

모두 default 옵션을 선택후, 버킷의 public access 설정을 아래의 링크를 참조하여 셋팅했다.

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/user-guide/block-public-access-bucket.html

버킷은 environment 변수 저장용 1개, 이미지 저장용 1개 통합 2개를 생성했다.

Laravel 프로덕션 환경용 .env 파일 s3 업로드

아래 명령으로 .env 파일을 s3 에 저장했다.

aws s3 cp .env.production s3://amuse-environment/.env

eb ssh 셋업

eb ssh --setup 을 입력하고 환경명을 지정하면, 다음과 같이 ssh 키를 생성후 업로드 할 수 있다.

eb ssh production 명령으로 해당 환경 EC2 인스턴스 ssh 접속이 가능해졌다.

.ebextensions 폴더 셋업

구체적인 셋업은 application specific 한 부분이므로, codebase 내에 있는 .config 파일들을 참고하기로 한다.

s3 bucket policy

웹앱에서 보여질 images 폴더에 버킷 정책을 아래와 같이 설정했다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::amuse-images/*"
            ]
        }
    ]
}

To-dos left

지금까지의 작업으로, EB 를 이용하여 하나의 코드베이스로 작업되어 있는 API 및 back-office 앱을 배포했고, 최소한의 기능이 정상 동작하는 것을 확인했다. 하지만, 아래와 같은 작업들이 추가로 진행되어야만 하는데, 이를 위한 필요한 설정 등은 기회가 되면 다음번에 올려보기로 한다.

CloudFront 구성

Posted by admin in Backend, 0 comments

Validating Android In-App Purchases with Laravel

Laravel 앱을 백앤드로 사용하는 안드로이드 프로젝트에서, 인앱결제 후 결제정보를 백앤드 API 을 이용하여 시스템에 반영하고 있었는데, 어느새 해당 API 를 임의로 호출하는 해킹 시도가 발생했다. 인앱 결제의 유효성 검증 단계 없이 저장하는 이런 보안에 취약한 API 를 위하여, 영수증 토큰값을 구글 API 를 통해 결제가 유효한지 검사하는 검증 로직과, 정기적으로 환불/주문취소/지불거절을 통해 무효화된 과거 인앱 주문에 대한 관리 로직을 추가했는데, 이번 포스팅에서 그 내용을 살펴본다.

아래 링크로 이동 하면 구글 개발자 콘솔상에서 해야할 내용들을 찾아볼 수 있다. (지만, 내용이 많고 장황한 설명이 제공되어, 원하는 내용을 한눈에 확인하기는 쉽지 않다.)

https://developers.google.com/android-publisher/getting_started?hl=ko#setting_up_api_access_clients

Google Developer Console

개발자 콘솔에 설정 > API 액세스 메뉴에서, 이미 등록된 OAuth 클라이언트와 서비스 계정이 없다면, 새롭게 만든다. 기등록된 서비스 계정이 있더라도, 해당 계정에 재무정보를 볼수 있는 권한이 없다면 해당 권한을 추가한다.

콘솔상에서, 서비스 계정을 생성시 다운받은 JSON credentials 파일은 서버의 적당한 위치에 복사하도록 한다. (본인의 경우 S3 에 저장한 다음, deploy 시에 base_path() 위치로 복사하는 방법을 사용했다.)

이제 구글 Play API 를 통하여 정상적인 결제 요청인지를 검증하는 API 와, 취소된 결제 리스트를 받아오는 API 를 위한 핸들러를 만들어야 하는데, 구글에서 제공하는 아래 패키지를 추가하면, OAuth 토큰 관리 등에 관한 로직을 직접 만들지 않아도 되므로, 간단히 작성할 수 있다.

composer require google/apiclient

위 패키지를 설치 후, 작성한 핸들러의 내용은 아래와 같다.

<?php

namespace App\Services;

use Exception;
use Google_Client;
use Google_Exception;
use Google_Service_AndroidPublisher;

class GooglePaymentHandler
{
    private $client;
    private $service;

    public function __construct(Google_Client $client)
    {
        $this->client = $client;
        $this->client->addScope(['https://www.googleapis.com/auth/androidpublisher']);
        $this->client->setAuthConfig(base_path() . '/google.credentials.json');
        $this->client->setIncludeGrantedScopes(true);
    }

    /**
     * 결제 검증 API
     * @param string $appName, (e.g. `개나소나`)
     * @param string $productCode, 안드로이드 상품코드 (e.g. `com.whatever.history.06`)
     * @param string $purchaseToken, 안드로이드 결제 영수증 코드
     * @return object|null
     * @throws Google_Exception
     */
    public function verify(string $appName, string $productCode, string $purchaseToken): ?object
    {
        $this->client->setApplicationName = $appName;
        $this->service = new Google_Service_AndroidPublisher($this->client);

        return $this->service->purchases_products->get(
            $this->getPackageName($appName), // com.some.thing
            $productCode,
            $purchaseToken
        );
    }

    /**
     * 취소한 결제 리스트 API
     * @param string $appName
     * @return object|null
     * @throws Exception
     */
    public function voidedPurchases(string $appName): ?object
    {
        $this->client->setApplicationName = $appName;
        $this->service = new Google_Service_AndroidPublisher($this->client);

        return $this->service->purchases_voidedpurchases->listPurchasesVoidedpurchases(
            $this->getPackageName($appName) // com.some.thing
        );
    }

    /**
     * convert app-name to package-name (com.some.thing)
     *
     * @param string $appName
     * @return string|null
     */
    private function getPackageName(string $appName): ?string
    {
        if ($appName === '개나소나') {
            return 'com.some.thing.xxx';
        }
        if ($appName === '돈크라이') {
            return 'com.some.thing.yyy';
        }
        if ($appName === '함흥차사') {
            return 'com.some.thing.zzz';
        }

        return null;
    }
}

각 API 로부터 리턴되는 객체에 대한 설명은 아래의 링크에서 볼 수 있다.

https://developers.google.com/android-publisher/api-ref/purchases/products/get
https://developers.google.com/android-publisher/api-ref/purchases/voidedpurchases/list

일반적이라면, verify() API 의 경우, 매 결제 요청시마다 호출하여, 그 validity 를 검사하도록 만들고, voidedPurchases() API 의 경우, CRON scheduler 로 등록한 Console Command 로 새벽시간에 1번 호출되도록 하여, 무효화된 결제에 관련된 사용자 구매 상태를 보정하도록 하면 되겠다.

Happy Coding!

Posted by admin in Backend, 0 comments

Laravel with Jenkins (5/5) – Wrap-up

이제 Jenkins 인스턴스에 접속하여 ansible 을 설치한다. (DO 에서 제공하는 레퍼런스 문서 링크 참고)

https://www.digitalocean.com/community/tutorials/how-to-install-and-configure-ansible-on-ubuntu-18-04

ansible 설치 후 로컬 PC 에 한것과 마찬가지로 /etc/ansible/digital_ocean.ini 와 /etc/ansible/digital_ocean.py 파일을 복사 후, jenkins 사용자 계정으로 Ansible 플레이북 (/home/jenkins/.ansible/deloy.yml) 이 정상적으로 실행되는지 확인한다.

ansible-playbook ~/.ansible/deploy.yml -i /etc/ansible/digital_ocean.py

위의 수동명령으로, api01 서버에 rolling deployment 실행을 할 수 있었다면, 라라벨 앱의 root 폴더에 있는 Jenkinsfile 의 25번째 라인에 deploy 스크립트 실행이 안되도록 comment out 시킨 부분을 찾아 # 문자를 삭제한다. 이렇게 함으로써, 새로운 commit 이 master 브랜치에 푸쉬될 때마다 새로운 도커 이미지가 api01 서버에 deploy 되는 것까지의 CI/CD 과정이 마무리 되는 것이다.

이제 마지막으로 api01 서버에 HTTPS 연결을 위하여, certbot 를 설치하고 nginx 설정을 변경해보자. Docker Swarm 모드로 nginx reverse proxy 를 설치할 때의 설정과 크게 다르지 않다.

Let’s Encrypt and Certbot

비영리 인터넷 보안 연구 그룹(ISRG)이 제공하는 Let’s Encrypt 를 사용하여, 90 일간 유효한 무료 Certificate 을 사용하면, 무료로 HTTPS 접속을 하도록 웹서버 설정이 가능하다. 이를 사용하기 위한 Python 스크립트 Certbot 과 Nginx 설정에 대한 훌륭한 문서들은 다양하게 존재하므로, 그 내용들은 아래의 링크들을 읽어보기 바란다.

일반적으로 Let’sEcrypt 인증방법은 DNS 인증방법이나, Webroot 방식을 선택하는데, 전자의 경우, 아래의 예시에서 처럼 certbot/certbot 도커 이미지를 이용하여 명령을 내리고, 해당 domain 의 DNS 에 TXT 레코드를 추가/수정하는데, 자동화를 위해서는 Terraform 명령을 실행하는 shell script 를 만들고 –manual-auth-hook 을 사용할 수 있다. 아니면, 후자의 방법을 선택하면 되는데, 이에 대한 설명은 위 레퍼런스 리스트 마지막 글에 자세히 설명되어 있어 생략한다.

docker run --rm -it \
   -v /opt/cert/key:/etc/letsencrypt \
   -v /opt/cert/lib:/var/lib/letsencrypt \
   -v /opt/cert/log:/var/log/letsencrypt \
   certbot/certbot \
   certonly \
     --manual \
     --preferred-challenges dns \
     -d "api.hanlingo.com" \
     --email [email protected] \
     --agree-tos

HTTPS 통신을 하려면, 위에서 생성한 Let’s Encrypt 인증서 volume 바인딩을 추가하여 재실행해야 하는데, Nginx 의 reverse proxy 설정은 아래처럼 변경한다.

upstream container {
  server app:80;
}
server {
  listen      80;
  listen [::]:80;
  server_name api.hanlingo.com;

  return 301 https://$host$request_uri;
}
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name api.hanlingo.com;

  ssl_certificate     /etc/letsencrypt/live/api.hanlingo.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/api.hanlingo.com/privkey.pem;
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers   HIGH:!aNULL:!MD5;

  location / {
    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_pass http://container;
    proxy_redirect off;

    # Handle web socket connection
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}

/opt/conf.d/proxy.conf 를 업데이트 했다면, 아래의 일련의 Docker 명령들을 다시 실행한다.

정상적으로 설정이 되었다면 브라우져 주소창에 https://api.example.com 을 입력하거나, 그냥 api.example.com 를 입력하면, nginx 에 의해 자동으로 https 리다이렉트된 https://api.example.com 주소에서, 정상적으로 앱이 실행되고 있음을 확인할 수 있다.

docker network create app-network

docker run -d \
   --name=app \
   --network=app-network \
   --restart=unless-stopped \
   registry.hanlingo.com/jinseokoh/laravel-app

docker run -d \
    --name=nginx \
    --network=app-network \
    --restart=unless-stopped \
    -v /opt/cert/key:/etc/letsencrypt \
    -v /opt/conf.d:/etc/nginx/conf.d \
    -p 80:80 \
    -p 443:443 \
    nginx:alpine

지금까지 5회에 걸쳐 Jenkins 를 이용한 PHP Laravel 앱의 CI/CD 설정에 대하여 알아보았는데. 다음에는 Docker Swarm 이나, 최근 GA 서비스로 확장된 DO 의 Kubernetes 에 배포하는 그림까지 확장해 보도록 하겠다.

So Stay tuned. Folks!

Posted by admin in Backend, 0 comments

Laravel with Jenkins (4/5) – Jenkins CI Pipelines

Jenkins 초기 화면

Jenkins 초기화면에서 Create New Jobs 를 누르고, Multibranch Pipeline 을 선택한다. 그 다음 입력화면에서, 프로젝트 이름, GitHub project URL, GitHub hook trigger for GITScm polling, Pipeline script 등 자신의 Jenkins 가 픽업해야할 GitHub 프로젝트에 대한 내용을 입력한다.

이 부분이 번거롭다 생각들면, Blue Ocean 플럭인을 설치 후, 왼쪽 메뉴바에서 Open Blue Ocean 메뉴를 선택 후,

  1. 새로운 Github Pipeline 생성,
  2. Github 페이지에서 Personal Access Token (Jenkins Blue Ocean 에 해당 링크가 나온다.) 를 생성후, 해당 토큰값을 입력,
  3. Github 사용자명을 선택하고 리파지토리 스캔 후 Jenkinsfile 이 있는 자신의 프로젝트 선택

과정을 걸쳐 GitHub 프로젝트를 선택할 수도 있다.

💡 ./Jenkinsfile

Jenkinsfile 파일은 Jenkins 의 파이프라인 즉, build => test => package => deploy 단계별 동작을 정의하는 파일이며, 프로젝트 root 에 아래와 같은 내용으로 만든다. (Jenkins classic theme 에서는 Pipeline 을 선택하면 BRANCH_NAME 환경변수가 셋팅되지 않으므로, Multibranch pipeline 이나 Freestyle 을 지정해야 하며, 임산부나 심신이 미약한 사용자는 셋팅이 좀더 쉬운 Blue Ocean 플럭인의 사용을 추천한다.)

#!/usr/bin/env groovy

node('master') {
    try {
        stage('build') {
            git url: '[email protected]:jinseokoh/nuxt-app-backend.git'
            sh "./h start"
            sh "./h composer install"
            // .env for testing
            sh "cp .env.example .env"
            sh "./h artisan key:generate"
        }

        stage('test') {
            sh "APP_ENV=testing ./h test"
        }

        stage('package') {
            if ("${env.BRANCH_NAME}" == 'master') {
                sh './docker/build'
            }
        }

        stage('deploy') {
            // sh "/usr/bin/ansible-playbook /opt/deploy.yml -i /etc/ansible/digital_ocean.py"

            sh "echo 'WE ARE DEPLOYING'"
        }
    } catch (error) {
        throw error
    } finally {
        sh "./h stop"
    }
}

위 Jenkinsfile 를 보면 BRANCH_NAME 이 master 인 경우에, ./docker/build 라 명명된 스크립트를 실행하여, 새로운 도커 이미지를 빌드하는 내용이 있는데, 이 스크립트는

  • 현재 master 브랜치의 최신 소스코드 내용을 appRoot/docker/app/packaged 라는 하위 폴더에 다운
  • 해당 폴더에 composer 로 dependency 설치
  • DO Spaces 로 부터 production 용 .env 파일 다운
  • 새로운 Docker 이미지 빌드 및 Docker registry 로 해당 이미지 푸시
  • 해당 폴더 삭제

와 같은 일련의 동작을 하도록 지정한 스크립트이며, 아래에 첨부한 내용과 같다.

💡 appRoot/docker/build

#!/usr/bin/env bash

# bail out on first error
set -e

# get the directory of the build script
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &amp;&amp; pwd )"

# get the current git commit sha
HASH=$(git rev-parse HEAD)

# package the app
cd $DIR/../
git archive --format=tar --worktree-attributes $HASH | tar -xf - -C $DIR/app/packaged

# production build steps
cd $DIR/app/packaged
./h composer install --no-dev

# get the production .env
s3cmd get s3://example-env/.env.production .env --force

# build the docker image with the latest code
cd $DIR/app
docker build \
  -t registry.example.com/jinseokoh/laravel-app:latest \
  -t registry.example.com/jinseokoh/laravel-app:$HASH .

# push images to private docker registry
docker push registry.example.com/jinseokoh/laravel-app:latest
docker push registry.example.com/jinseokoh/laravel-app:$HASH

# clean up packaged directory
cd $DIR/app
PWD=$(pwd)
if [ "$PWD" == "$DIR/app" ]; then
  docker run --rm -w /opt -v $(pwd):/opt ubuntu:18.04 bash -c "rm -rf packaged"
  mkdir packaged
  touch packaged/.gitkeep
fi

Private Docker Registry

따라서, 빌드한 도커 이미지를 저장할 private docker registry 가 필요하다. private docker registry 을 생성하는 방법은 다양한 옵션이 가능하지만, 개인적으로는 아래와 같은 간단한 절차로 Docker Registry 를 별도의 DO 인스턴스에 만들어, https://registry.example.com 라는 주소에 접속할 수 있도록 만들었다.

  • docker-machine 으로 registry 인스턴스 생성
  • DO DNS 셋업
  • docker-compose / nginx / htpasswd / cerbot 패키지 설치
  • nginx port forwarding 설정
  • nginx.conf 에 client_max_body_size 2G 추가

단계별 상세 설명은 아래 문서를 참고하자.

https://www.digitalocean.com/community/tutorials/how-to-set-up-a-private-docker-registry-on-ubuntu-18-04

💡 docker-compose.yml

최종적인 docker-compose 파일의 내용은 아래와 같다.

version: '3'

services:
  registry:
    image: registry:2
    ports:
    - "5000:5000"
    environment:
      REGISTRY_AUTH: htpasswd
      REGISTRY_AUTH_HTPASSWD_REALM: Registry
      REGISTRY_AUTH_HTPASSWD_PATH: /auth/registry.password
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
    restart: unless-stopped
    volumes:
      - ./auth:/auth
      - ./data:/data
      - ./config.yml:/etc/docker/registry/config.yml
💡 config.yml

private docker registry 의 커스텀 config.yml 의 내용은 아래와 같다. 이미지 삭제를 enable 시킨 내용이 추가되었다.

version: 0.1
log:
  fields:
    service: registry
storage:
  cache:
    blobdescriptor: inmemory
  delete:
    enabled: true
  filesystem:
    rootdirectory: /var/lib/registry
http:
  addr: :5000
  headers:
    X-Content-Type-Options: [nosniff]
health:
  storagedriver:
    enabled: true
    interval: 10s
    threshold: 3

Jenkins 인스턴스에서, 다음과 같은 docker login 명령을 실행하여, 레지스트리 접속 credentials 정보를 입력한다.

sudo docker login --username=jenkins registry.example.com

만일 Error saving credentials: error storing credentials - err: exit status 1, out: Cannot autolaunch D-Bus without X11 $DISPLAY 와 같은 에러 메시지가 나온다면,

아래의 명령을 입력하여 필요한 패키지들을 설치 후 다시 실행하면 된다.

sudo apt install gnupg2 pass

이제 아래의 curl 명령으로 레지스트리로부터 {"repositories":[""]} 라는 응답 오는 것을 확인했다면, 정상적으로 동작하는 private docker registry 를 갖게 되었다는 의미이다. Congrats!

curl -u jenkins:{your-password} https://registry.example.com/v2/_catalog

물론, 장기적인 운영을 하다보면, 일정 갯수 이상의 이미지를 보관할 필요는 없으므로, private docker registry 의 house keeping 을 해줄 필요가 있을 터이나, 이 문서의 scope 을 벗어나는 내용이므로, 실제 도커 레지스트리 운영에 관심이 있다면 아래 링크를 참고하기 바란다.

https://stackoverflow.com/questions/29802202/docker-registry-2-0-how-to-delete-unused-images

Jenkins in Action for Production Images

이제 GitHub 에 새로운 commit 을 push 하거나, Build Now 메뉴를 클릭하면, 아래 이미지와 같이 파이프라인별 진행 상황을 볼 수 있다.

젠킨스 Classic Theme 동작 화면

만일, Jenkins Blue Ocean 플럭인을 선택하였다면, 아래와 같은 보다 개선된 UI 를 볼 수 있다.

Jenkins Blue Ocean 파이프라인화면

젠킨스 CI/CD 과정이 처음으로 성공하였다면, 빌딩된 도커이미지가 private docker registry 에 {“repositories”:[“jinseokoh/laravel-app”]} 와 같이 등록된 것을 볼 수 있어야만 하며, Jenkins 인스턴스에서도 그 이미지가 아래처럼 리스팅 되어야만 한다.

Jenkins 인스턴스에서 docker images 확인

지금까지의 파이프라인 동작으로, GitHub 계정의 Laravel PHP 앱 프로젝트 리포에 master 브랜치를 배포하면, Jenkins 가 그것을 픽업하여 Jenkins 인스턴스의 Docker 환경에서 테스트를 진행하고, 오류가 발생하지 않으면 Docker 이미지를 빌드한 다음, 별도의 인스턴스에 구축한 private docker registry 에 푸쉬하는 과정을 살펴보았다. 이제 프로덕션 코드를 위한 새로운 인스턴스가 필요하다.

Deploy Laravel-app to production droplet

프로덕션용 Laravel PHP 앱이 빌딩되어 도커 이미지로 registry 에 푸쉬되었다면, 그 이미지를 실행할 “Docker 가 설치된 인스턴스”에 배포해야 하는데, 그 인스턴스는 이미 앞서 언급했 듯, Terraform/Ansible 조합을 이용하여 생성한다. 이전 Terraform 포스팅 설명 내용과 함께 비교해 보면, 각각의 차이점을 볼 수 있는 좋은 예시가 되리라 본다.

Terraform

이전 포스팅에서는, 테라폼 변수 내용을 명령행 인자로 넘겨주는 방법을 쓴 것에 비해, 이번에는 테라폼 변수 내용을 terraform.tfvars 파일에 작성하고 이를 픽업하도록 만드는 방법을 사용했다.

💡 terraform.tfvars

아래와 같이, terraform.tfvars 을 작성한다. 이 파일 이름을 .gitignore 에 등록하여 유출을 막도록 하자. fingerprint# 값은 DO 에 등록된 SSH public 키의 해쉬값이다. (즉 3개의 머신으로부터, 새롭게 만들어지는 DO 인스턴스에 SSH 접속이 가능하도록 만들기 위함이다.)

do_api_token = "d8a22e27a1b440fa4a91a6f65e0cbfd1f42a781df5a508bfae2fa7127a6axxxx"
fingerprint1 = "1f:8f:97:8b:1d:c2:83:d9:24:40:81:3d:93:94:xx:xx"
fingerprint2 = "ce:33:72:a6:03:bc:ae:21:5c:d8:de:10:67:ac:xx:xx"
fingerprint3 = "af:45:a7:88:d5:60:e3:b1:9d:a5:92:0f:5c:22:xx:xx"
💡 main.tf
variable "do_api_token" {}
variable "fingerprint1" {}
variable "fingerprint2" {}
variable "fingerprint3" {}

## cloud privider
provider "digitalocean" {
  token = "${var.do_api_token}"
}

## firewall
resource "digitalocean_firewall" "api" {
  name = "api"

  droplet_ids = [
    "${digitalocean_droplet.api.id}",
  ]

  inbound_rule = [
    {
      protocol   = "tcp"
      port_range = "22"
    },
    {
      protocol   = "tcp"
      port_range = "80"
    },
    {
      protocol   = "tcp"
      port_range = "443"
    },
  ]
}

## instance
resource "digitalocean_tag" "api" {
  name = "api"
}

resource "digitalocean_droplet" "api" {
  name               = "api01"
  image              = "ubuntu-18-04-x64"
  region             = "sgp1"
  size               = "s-1vcpu-1gb"
  private_networking = true
  monitoring         = true

  tags = [
    "${digitalocean_tag.api.name}",
  ]

  ssh_keys = [
    "${var.fingerprint1}",
    "${var.fingerprint2}",
    "${var.fingerprint3}",
  ]
}

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

위 설정을 갖고 terraform init, terraform plan, terraform apply 를 차례로 진행하면 api01 라는 이름의 인스턴스를 생성할 수 있다. 다음으로, 이 인스턴스의 설정을 위해 Ansible 을 사용해보자.

Ansible to the rescue!

아직 Ansible 을 설치하지 않았다면 homebrew 를 이용하여 설치하고, /etc/ansible 폴더에 Ansible 에서 배포하는 DigitalOcean external inventory script (digital_ocean.pydigitial_ocean.ini) 를 아래 링크로부터 복사한 다음, pip install requests 명령을 내리면 필요한 디펜던시들이 설치된다.

https://github.com/ansible/ansible/tree/devel/contrib/inventory

/etc/ansible/digital_ocean.ini 파일의 내용 중 아래 2개 변수를 수정하고

api_token = your_digital_ocean_api_token_goes_here
:
group_variables = { 'ansible_user': 'root' }

스크립트가 동작하는지 확인하기 위하여, 아래 명령을 내려본다.

/etc/ansible/digital_ocean.py --pretty

자신의 DO 인벤토리 내용이 정상적으로 출력되면, 이제 첫번째 ansible playbook 을 만들 차례이다.

💡 playbook.yml

플레이북은 yaml 파일로 정의하며, 각 task 별 수행내용을 name 키에 적어 놓았으므로 참고하기 바라며, 전체 파일은 https://github.com/jinseokoh/ansible 에서 다운받을 수 있다.

---
- hosts: api01
  gather_facts: false
  become: true
  vars:
    ansible_python_interpreter: /usr/bin/python3

  tasks:
    - name: "sudo apt update"
      apt:
        update_cache: yes
      tags:
        - system

    - name: "APT - 도커 GPG key 추가"
      apt_key:
        url: https://download.docker.com/linux/ubuntu/gpg
        state: present

    - name: "APT - 도커 리파지토리 추가"
      apt_repository:
        repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
        state: present
        filename: docker

    - name: "APT - 기본 패키지 설치"
      apt:
        pkg:
          - "apt-transport-https"
          - "ca-certificates"
          - "curl"
          - "vim"
          - "python3-pip"
          - "software-properties-common"
        state: present

    - name: "PIP - docker-py 패키지 설치"
      pip:
        name: "docker-py"

    - name: "APT - 도커 설치"
      apt:
        name: "docker-ce"
        update_cache: yes

    - name: "도커 - 레지스트리 로그인"
      docker_login:
        registry: registry.hanlingo.com
        username: jenkins
        password: dhfgoRhr1djrekftjd!
        reauthorize: yes

    - name: "디렉토리 생성 - /opt/conf.d"
      file:
        path: /opt/conf.d
        state: directory

    - name: "파일복사 - nginx reverse proxy 설정 config"
      copy:
        src: "./files/nginx.conf"
        dest: "/opt/conf.d/proxy.conf"
        mode: 0644

    - name: "파일복사 - rolling deployment bash script"
      copy:
        src: "./files/deploy.sh"
        dest: "/opt/deploy"
        mode: 0755

    - name: "sudo apt autoclean"
      apt:
        autoclean: yes
      tags:
        - system

    - name: "sudo apt autoremove"
      apt:
        autoremove: yes
      tags:
        - system

아래와 같이 playbook 실행 명령을 내리면

ansible-playbook ./playbook.yml -i /etc/ansible/digital_ocean.py

Terraform 으로 만든 DO 프로덕션용 api01 인스턴스에, 도커를 실행시키기 위한 모든 패키지 설정이 한방에 이뤄진다. 아래의 명령으로 레지스트리로부터 도커 이미지를 다운 받아 실행시켜보자.

docker run -d -p 80:80 \
   --restart=unless-stopped \
   --name=app \
   registry.example.com/jinseokoh/laravel-app

그러면 아래와 같이 app 이라 명명된 하나의 이미지가 돌아가는 것을 docker ps 명령으로 확인 할 수 있다.

docker-01 인스턴스에 프로덕션 코드 배포

브라우저를 이용하여 http://api.example.com 주소로 접속해 보면, 정상적으로 Laravel 앱이 실행되는 걸 확인 할 수 있다. Yay!

Rolling deployment

위에서는 빌딩한 이미지를 api01 인스턴스의 80 포트에 바인딩해서, 아래와 같이 실행하였는데

browser
   └─ droplet (api01)
     └─ app 이미지 컨테이너

rolling deployment 스타일 배포를 위하여, 아래처럼 nginx 도커 이미지를 이용하여, nginx reverse proxy 를 거쳐 app 이미지 컨테이너로 트래픽을 전달하게 만든다.

browser
   └─ droplet (api01)
     └─ nginx reverse proxy
       └─ app 이미지 컨테이너

위의 playbook.yml 58~62번째 줄을 보면 nginx.conf 을 /opt/conf.d/proxy.conf 로 복사하는 내용이 있는데, 이것이 바로 nginx reverse proxy 를 위한 nginx 설정이며, 그 내용은 아래와 같다.

💡 /opt/conf.d/proxy.conf
upstream container {
  server app:80;
}

server {
  listen 80 default_server;
  server_name _;

  location / {
    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_pass http://container;
    proxy_redirect off;

    # Handle web socket connection
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
  }
}

nginx reverse proxy 설정 파일이 이미 인스턴스 안에 존재하므로, 현재 그 인스턴스에서 실행중인 컨테이너를 중단 (docker stop app) 하고, 삭제 (docker rm -v app) 한 다음, 아래의 커맨드라인 Docker 명령을 하나씩 실행해 보자.

docker network create app-network

docker run -d \
  --name=app \
  --network=app-network \
  --restart=unless-stopped \
  registry.hanlingo.com/jinseokoh/laravel-app

docker run -d \
  --name=nginx \
  --network=app-network \
  --restart=unless-stopped \
  -v /opt/conf.d:/etc/nginx/conf.d \
  -p 80:80 \
  nginx:alpine

이전과 마찬가지로, 브라우저를 이용하여 http://api.example.com 주소로 접속해 보면 Laravel PHP 앱이 정상 동작하는 것을 확인할 수 있어야 한다. nginx reverse proxy 가 제대로 동작한다면, 마지막으로, 새로운 도커 이미지가 만들어질때마다 어떻게 rolling deployment 작업을 수행할지 각 단계별 수행 내용들을 생각해 보자.

  1. PHP 앱의 latest 이미지를 registry 로부터 다운
  2. 새롭게 다운받은 이미지로 도커 컨테이너 실행
  3. nginx 가 트래픽을 새로 실행한 도커 컨테이너로 보내도록 config 변경 후 재실행
  4. 이전 컨테이너 실행 중단/제거 후 이미지 삭제

위 리스트에 나열된 작업들을 차례대로 수행하는 것이 바로 rolling deployment 를 수행하는 것이며, 이를 위한 bash 스크립트는 이미 작성하여, 위 playbook.yml 64~68 번째줄의 task 로, /opt/deploy 위치에 복사해 놓았다. 그 내용을 살펴보면 아래와 같다.

💡 /opt/deploy

주석문의 번호와 위 설명 리스트의 번호가 일치하므로 참고한다.

#!/usr/bin/env bash

APP_CONTAINER=$(sudo docker ps -a -q --filter="name=app")
NEW_CONTAINER="app`date '+%y%m%d%H%M%S'`"
REGISTRY="registry.hanlingo.com/jinseokoh/laravel-app"
DANGLING_IMGS=$(sudo docker image ls -f "dangling=true" -q)
RUNNING_IMG=$(sudo docker inspect $(sudo docker ps -a -q --filter="name=app") | grep -m 1 -o 'sha256[^"]*')
CURRENT_IMG=$(sudo docker image inspect $REGISTRY | grep -m 1 -o 'sha256[^"]*')

# 1) pull the latest image
sudo docker pull $REGISTRY

# avoid deployment if running image is latest
if [ "$CURRENT_IMG" == "$RUNNING_IMG" ]; then
    echo "The latest image is already in use."
    exit 0
fi

# 2) spin off new instance
NEW_APP_CONTAINER=$(sudo docker run -d --network=app-network --restart=unless-stopped --name="$NEW_CONTAINER" $REGISTRY)

# wait for processes to boot up
sleep 5
echo "Started new container $NEW_APP_CONTAINER"

# 3) update nginx
sudo sed -i "s/server app.*/server $NEW_CONTAINER:80;/" /opt/conf.d/proxy.conf

# config test
sudo docker exec nginx nginx -t
NGINX_STABLE=$?

if [ $NGINX_STABLE -eq 0 ]; then
    # reload nginx
    sudo docker kill -s HUP nginx

    # 4) stop older instance
    sudo docker stop $APP_CONTAINER
    sudo docker rm -v $APP_CONTAINER
    echo "Removed old container $APP_CONTAINER"

    # cleanup
    if [ ! -z "$DANGLING_IMGS" ]; then
        sudo docker image rm $DANGLING_IMGS
    fi
else
    echo "ERROR: nginx config test failed."
    exit 1
fi

api01 인스턴스에 접속 후 커맨드라인에서 /opt/deploy 라는 명령을 내리면, 아래 스크린 샷에서 확인 할 수 있는 것 처럼 새로운 도커 이미지로 rolling update 가 이뤄지는 것을 볼 수 있다.

따라서, Jenkins 인스턴스에서 api01 인스턴스에 존재하는 /opt/deploy script 를 실행시길 방법이 필요한데, 이는 ansible 을 사용하여 다음과 같이 작성했다.

💡 deploy.yml

로컬 PC 에서 아래와 같이 ansible playbook 명령을 실행하면, Ansible 을 이용하여, api01 인스턴스의 deploy 스크립트를 실행할 수 있음을 확인할 수 있다.

ansible-playbook ./deploy.yml -i /etc/ansible/digital_ocean.py
---
- hosts: api01
  gather_facts: false
  become: true
  vars:
    ansible_python_interpreter: /usr/bin/python3

  tasks:

    - name: "run rolling deployment bash script"
      command: "sh /opt/deploy"

지금까지 많은 작업을 했는데, 이제 다음 포스팅에서는

  1. Jenkins 인스턴스에서도 위의 playbook 파일을 실행할 수 있도록 Ansible 설치와 설정을 하고,
  2. Laravel PHP 앱 코드베이스에 있는 Jenkinsfile 의 내용을 수정하여 registry 로부터 다운받은 이미지로 rolling deployment 를 하게 만들고
  3. 더불어, certbot 을 이용하여, HTTPS 접속이 가능하도록 만드는 작업 등

에 대하여, 설명을 이어가겠다.

Posted by admin in Backend, 0 comments