UzukiLive 서버 현황

Vultr VC2가 같은 가격에 SSD 용량을 추가로 제공한다는 사실을 이제 깨달은 나머지(…) 전체적으로 UzukiLive 서버를 유지보수 하면서 작업을 진행했는데, 차후 서버를 옮길 필요가 있을 때 참고를 해야 할 경우가 있어서 업데이트 겸에 정리를 진행했습니다.

UzukiLive 서버란?

UzukiLive 서버는 소유중인 도메인인 ‘uzuki.live’ 라는 도메인 이름에서 파생된 서버 이름이고, Uzuki 라는 이름의 모티브는 ‘アイドルマスターシンデレラガールズ’ 의 메인 캐릭터인 ‘島村卯月'(Uzuki Shimamura) 에서 가져왔습니다.

이 서버는 2018. 12. 22 ~ 2018. 12. 25 일에 진행된 ‘UzukiLive 새 인스턴스 확장 이전'(https://pyxispub.uzuki.live/?p=1463) 으로 생성된 서버로, 기존 Vultr VC2 1Core 2GB , Vultr VC2 2Core 4GB 인스턴스를 합치고 새로 생성한 서버입니다. 리전은 Tokyo로 등록되어 있습니다.

이 서버의 목적은 개인 운용 목적의 서버로 블로그 서버 + 메일 서버 + 클라우드 서버 기능을 포함하며, 기타 안드로이드 개발에 필요한 Jenkins CI + JFrog Artifactory를 포함합니다.

그 외, 서버를 원할하게 관리하기 위한 Monitoring 스택 (cAdvisor + NodeExporter + Prometheus + Grafana) 을 구현하여 실제 프로덕션 환경에서 최소한으로 관리할 수 있는 능력에 대한 연습 서버이기도 합니다.

도메인은 NameCheap, Inc에서 등록되었고, DNS 네임서버는 dnsever에서 관리되고 있습니다.

서버 구조 개요

UzukiLive는 Vultr Cloud Compute (VC2) 의 4 CPU 8 GB 160GB SSD 기반의 클라우드에 Ubuntu 18.04.2 LTS + Docker 18.09.5 버전으로 실행중인 서버로, 현재 약 21개의 도커 인스턴스가 실행되고 있습니다.

  • Nginx – Proxy-pass, SSL (Let’s Encrypt) 연결을 위한 서비스
  • Service Layer
    • WordPress – PyxisPub 발행을 위한 워드프레스 서비스
    • Jenkins – Android 및 Docker CI/CD 작업을 위한 Jenkins 서비스
    • Artifcatory – 비공개 그레들 아키펙트 배포 및 기존 아키펙트 레포지토리에 대한 캐시 서비스
    • OwnCloud – 개인 저장용 클라우드 서비스
    • Rainloop – 메일 서버를 위한 프론트엔드 UI
  • Internal Layer
    • Docker-mailserver – 개인 이메일 (pyxis@uzuki.live) 및 부가 이메일 서비스를 위한 메일 서버 (IMTP(POP3)/SMTP 구현 포함)
    • Shadowbox – Jigsaw 가 개발하는 Outline 의 기반 컨테이너
    • Redis – Redis 데이터베이스, OwnCloud 전용
    • MariaDB – MariaDB 데이터베이스, WordPress 전용
    • pure-ftpd – (S)FTP 제공
    • Adminer – MariaDB에 대한 web admin
  • Monitor Layer
    • cAdvisor – 도커 엔진, 컨테이너, 이미지 등에 대한 데이터를 수집하는 모니터링 도구
    • NodeExporter – 호스트 서버를 모니터링 하는 도구
    • Prometheus – 컨테이너로부터의 데이터를 받기 위한 중간 매개체 도구
    • Grafana – 여러 데이터 제공자로부터 데이터를 받아 그래프로 보여주는 도구
    • AlertManager – Prometheus로부터 데이터를 전달받아 이벤트가 발생되었을 때 정해놓은 알림 룰을 통해 전송하는 도구
    • PushGateway – Prometheus 에 일회성/배치 작업에 대한 통계를 전송하도록 하는 도구

모든 레이어는 단일 uzukilive-network 네트워크로 엮여져있으며, 각 레이어마다 내부 통신을 위한 네트워크가 별도로 구성되어 있습니다.

외부에 포트 노출이 필요한 Nginx, MailServer, ShadowBox, pure-ftpd 을 제외하고는 내부 포트를 통해 통신하게 되며, 내부간 통신이 필요할 경우 uzukilive-network 네트워크와 각 레이어마다 할당된 네트워크를 통해 구현하게 됩니다.

Service Layer와 Grafana는 실제 외부에서 접근 가능한 서비스들이지만 이 포트들은 외부에 노출되지 않고 Nginx를 통해 Proxy-pass하게 됩니다. 즉, Nginx가 각 서비스에 접근하기 위한 게이트웨이로 작용하게 되어 서브도메인으로 구분하게 됩니다.

이 때, http로 접속하였을 경우 https로 리다이렉트하는데, 이 https에 대한 인증서는 Let’s Encrypt를 통해 발급되었습니다.

서버 업데이트 내역

2019.04.28 이후 내역을 정리합니다.

  • 2019.04.28
    • 사양 업그레이드 – VC2 4core 8GB 100GB SSD -> VC2 4core 8GB 160GB SSD
    • 아키펙트 레포지토리 캐시 서버 cron 개선 – 매 정각 12시마다 336시간 동안 사용되지 않은 아키펙트 삭제 -> 매 정각 12시마다 168시간 동안 사용되지 않은 아키펙트 삭제
    • 사용하지 않는 Expose된 포트 제거
    • cAdvisor, Prometheus, AlertManager, PushGateway 등이 어떠한 경로를 통하여 외부에서 접근이 가능한 우회 방법 방지
    • 사용하지 않는 A 레코드 정보 제거

Build and Deploy Dockerfile in Jenkins

[Start RESTful Service With Spring Boot 2.x + Kotlin] 글에서 유일하게 다루지 못했던 주제가 있었다. 바로 Jenkins를 이용한 CI/CD 연동이다.

글을 올린지 약 1달 반 정도 지났지만, 성공적으로 빌드할 수 있게 되었고 이를 작성해보려 한다.

환경설정

Jenkins 내부에서 Docker를 빌드하려면 Docker 바이너리가 Jenkins에 포함되어 있어야 한다. Jenkins 역시 도커 이미지로 배포되기 때문에, 먼저 커스텀 Jenkins 이미지를 수정할 필요성이 있다.

Docker + Jenkins 연동은 [Run Docker + Jenkins for Android Build] 글에서 다루니 그쪽부터 보면 좋을 것 같다.

Docker를 설치하기 위해서 추가적으로 필요한 의존성은 다음과 같다.

  • ca-certificates
  • curl
  • gnupg2
  • file
  • lxc
  • apt-transport-https

이 6개를 apt install로 통해 설치하고 순서대로 명령어를 입력하면 된다.

## Install requirements
RUN dpkg --add-architecture i386
RUN rm -rf /var/lib/apt/list/* && apt-get update && apt-get install ca-certificates curl gnupg2 software-properties-common git unzip file apt-utils lxc apt-transport-https -y

그 다음, docker 서버로부터 인증서와 레포지토리 주소를 받아서 설치하면 된다.

## Install Docker-ce into Image
RUN curl -fsSL https://download.docker.com/linux/$(. /etc/os-release; echo "$ID")/gpg > /tmp/dkey;
RUN apt-key add /tmp/dkey
RUN add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/$(. /etc/os-release; echo "$ID") $(lsb_release -cs) stable"
RUN apt-get update && apt-get install docker-ce -y --no-install-recommends
RUN usermod -a -G docker jenkins

여기까지 마치면 Jenkins 이미지에 Docker-CE 바이너리가 설치된다.

아니면, 업데이트 된 [WindSekirun/Jenkins-Android-Docker] 이미지를 활용해도 문제는 없다.

JenkinsFile 작성

Spring-Boot 프로젝트를 빌드하고, 이를 Docker로 배포하는 과정에 있어서 Jenkins에 필요한 작업을 정의하는 작업이 필요하다.

Jenkins는 각 파이프라인 상에서 수행해야 될 작업을 Jenkinsfile 이라는 파일에 정의하고, 이를 빌드할 때 마다 사용할 수 있다.

필요한 작업을 정리하면 다음과 같을 것이다.

  • 환경설정
  • Gradle 빌드
  • DockerImage 빌드
  • DockerHub로 이미지 푸시
  • 빌드된 이미지 제거

이 중, gradle 까지의 작업을 반영하면 다음과 같다

pipeline {
    agent any
    stages {
        stage('Environment') {
            parallel {
                stage('chmod') {
                    steps {
                        sh 'chmod 755 ./gradlew'
                    }
                }
                stage('display') {
                    steps {
                        sh 'ls -la'
                    }
                }
            }
        }
        stage('Build Jar') {
            steps {
                sh './gradlew build'
            }
        }
     }
}

Docker 이미지 빌드

환경변수에 registry 이라는 변수를 정의한다.

environment {
        registry = "windsekirun/uploadfileboot"
}

registry 는 이미지에 대한 태그이며, https://hub.docker.com/r/windsekirun/uploadfileboot 이런 식으로 접근이 가능해진다.

그 다음 docker 빌드 명령어를 작성한다. 기본적인 빌드 명령어는 docker build -t windsekirun/uploadfileboot:latest .이므로 Jenkinsfile의 형식에 맞게 작성하면 된다.

stage('Build docker image') {
            steps {
                sh 'docker build -t $registry:latest .'
            }
}

latest 대신 $BUILD_NUMBER 나 다른 환경변수를 이용해서 사용도 가능하다.

빌드된 이미지 푸시

Jenkins 설정 > 인증정보에 정보를 추가한다

그리고 환경변수에 추가한 정보의 id를 정의하면, 준비는 완료된다.

environment {
        registry = "windsekirun/uploadfileboot"
        registryCredential = 'dockerhub'
}

마지막으로, registry에 푸시하는 명령어는 docker push windsekirun/uploadfileboot:latest 이므로, Jenkinsfile 형식에 맞춰 작성한다. 다만 crediental 정보가 필요하므로 withDockerRegistry 명령어를 통해 crediental 정보를 입력한다.

stage('Deploy docker image') {
            steps {
                withDockerRegistry([ credentialsId: registryCredential, url: "" ]) {
                    sh 'docker push $registry:latest'
                }
            }
}

빌드한 이미지 삭제

빌드된 이미지를 계속 남겨둘 필요는 없으므로, 지금까지 빌드한 이미지를 삭제한다.

stage('Clean docker image') {
            steps{
                sh "docker rmi $registry"
            }
}

전체 코드

pipeline {
    environment {
        registry = "windsekirun/uploadfileboot"
        registryCredential = 'dockerhub'
    }
    agent any
    stages {
        stage('Environment') {
            parallel {
                stage('chmod') {
                    steps {
                        sh 'chmod 755 ./gradlew'
                    }
                }
                stage('display') {
                    steps {
                        sh 'ls -la'
                    }
                }
            }
        }
        stage('Build Jar') {
            steps {
                sh './gradlew build'
            }
        }
        stage('Build docker image') {
            steps {
                sh 'docker build -t $registry:latest .'
            }
        }
        stage('Deploy docker image') {
            steps {
                withDockerRegistry([ credentialsId: registryCredential, url: "" ]) {
                    sh 'docker push $registry:latest'
                }
            }
        }
        stage('Clean docker image') {
            steps{
                sh "docker rmi $registry"
            }
        }
     }
}

이 Jenkinsfile 를 가지고 Jenkins를 설정하면 다음과 같이 나오게 된다.

마지막으로 push된 이미지는 Dockerhub 홈페이지에서 확인할 수 있다.

마무리

Jenkins 를 이용해 도커 이미지를 빌드하고 배포하는 과정까지 살펴보았다.

물론 master에 빌드한 것을 그대로 push하는 것은 매우 위험하기 때문에 개발하는 사이클마다 별도로 적용해야 되지만, 개론적인 것은 살펴본 것과 크게 다르지는 않을 것이다.

위에 살펴본 DockerHub에 올리는 것 말고도 private registry에 올리는 것 또한 가능하기에 선택하기 나름일 것 같다.

Diary #1, Dagger의 Child Fragment 관련

주의사항!

이 포스트는 주인장(@WindSekirun) 과 여러 사람들과 개발(특히 Android 관련이 많습니다)에 관련된 이야기를 할 때, 간단히 다루기 좋은 이야기를 있는 그대로 표현하는 포스트입니다. 대화 내용은 보기 좋게만 변경하고, 오탈자는 수정하지 않습니다. 만일 대화중 틀린 내용이나 보강할 내용이 있다면 댓글로 알려주시면 감사합니다!

아니면 텔레그램 WindSekirun 으로 주셔도 됩니다.

이번 주제

Dagger에서 ChildFragment를 ParentFragment가 관리해야 되는 것인가, 아니면 Activity가 관리해야 되는 것인가?

등장인물

  • Pyxis, 주인장
  • @Zeallat , 언제나 도움을 주셔서 매우 감사한 분입니다.

정답

등잔 밑이 어둡다고, Dagger 공식 레포 상에 답이 있습니다. 링크는 [여기] 로, DialogFragment의 베이스 클래스의 구현체입니다. 보면 onAttach에서 AndroidSupportInjection 을 통해 injection 하는 것을 확인할 수 있습니다.

즉 ChildFragment는 Parent Fragment가 관리하는 게 맞고, injection 은 위를 참고하면 됩니다

대회내용

  • Pyxis: 무튼
  • Pyxis: Dagger 에 스코프가 있을 때
  • Pyxis: Activity : Service: BroadcastReceiver: ContentProvider는
  • Pyxis: Application 스코프잖아요
  • Pyxis: 그래서 DispatchingAndroidInjector 를 Application에서
  • Pyxis: 구현하고
  • Pyxis: 그리고 Fragment는
  • Pyxis: 공식 컴포넌트가 아니고: Activity에 종속적이기 때문
  • Pyxis: 에
  • Pyxis: Activity에 종속적이고
  • Pyxis: 그리고 DialogFragment가 있어요
  • Pyxis: Fragment도 Activity 종속이고: 다이얼로그도 Activity종속이라 볼 수 있어요 (Window Token 관련 살펴보면 이렇게 결론 내릴 수 있음)
  • Pyxis: 여기서: DialogFragment 안에 ChildFragment가 있다고 할때
  • Pyxis: 이 Fragment는 Activity에 관리하는게 맞을까요
  • Pyxis: 아니면 parnet fragment (여기서는 DialogFragment)가 관리해야될까요
  • Zeallat: 스코프를요?
  • Pyxis: DispatchingAndroidInjector를 받아서
  • Zeallat: activity life cycle에 맞추느냐
  • Pyxis: HasSupportFragmentInjector 같은거
  • Pyxis: 구현하고
  • Zeallat: fragment lifecycle에 맞추느냐
  • Zeallat: 그건가여?
  • Pyxis: 라이프사이클 관련
  • Pyxis: 네 어떻게 보면 라이프사이클이네여
  • Pyxis: 저는 진짜 왠만하면
  • Pyxis: child fragment는 안만들려고 하거든요
  • Pyxis: 예전에 당한게 하도 많아서
  • Zeallat: 어떤 케이스이죠?
  • Pyxis: 그래서 저 경우를 아예 생각하고 있지 않았는데
  • Pyxis: DialogFragment 하나로
  • Pyxis: 여러개의 Fragment를 replace?
  • Pyxis: 하는 그런목적인거같아요
  • Pyxis: 지금 구성하는 앱이
  • Pyxis: 연속적으로 다이얼로그만 4~6개 뜨게 되있어서
  • Pyxis: 하나하나 dismiss해서 하는거보다 내부에서 관리하는게
  • Pyxis: 더 효율성있다고는 보거든요
  • Zeallat: 음
  • Zeallat: 그러니까
  • Zeallat: 페이지 하나에서 fragment(팝업)을 여러개 표시해야하는데
  • Zeallat: 그걸 replace로 하기위해서
  • Zeallat: dialog 하나 만들고 그안에서
  • Zeallat: child 여러개를 컨트롤한다는 이야기인가요?
  • Pyxis: Exactly
  • Zeallat: 그런데
  • Zeallat: 이 parent fragment가
  • Zeallat: 여러 activity에서 불리는 시츄에이션인가요?
  • Pyxis: 아뇨
  • Pyxis: 기획적으로는 하나의 activity
  • Pyxis: 에서만 불리는데
  • Zeallat: 그럼 왜 굳이 parent를 만들어요?
  • Pyxis: 한 화면내에서 여러번 불릴 가능성은
  • Pyxis: 있어요
  • Zeallat: 그냥 activity에서 replace 컨트롤 하면 안되나요?
  • Pyxis: activity는
  • Zeallat: 아니면 공통 view를 공유하나요?parent의?
  • Pyxis: 그대로 두고
  • Pyxis: parent의 공통 뷰를
  • Pyxis: 공유하죠
  • Pyxis: parent dialog에는 타이틀이랑 x버튼
  • Pyxis: 같은거 두고
  • Pyxis: 그 안 fragment에서는
  • Pyxis: N:1 관계로
  • Pyxis: (1이 ViewModel)
  • Pyxis: 상태 관리하고
  • Pyxis: 대충 어떤 시나리오인지
  • Pyxis: 예상가시나여?
  • Zeallat: 네 이해가요
  • Zeallat: parent fragment에서 단순 호출이아니고
  • Zeallat: parent의 일부 뷰에만
  • Zeallat: child를 표시한다는거죠?
  • Pyxis: 네
  • Pyxis: AndroidSupportInjection.inject 코드 보니까
  • Pyxis: parent fragment를 찾는거같은 코드가
  • Pyxis: 있더라고요
  • Pyxis: 그렇게 되면 제 생각은
  • Pyxis: ParentFragment 가 ChildFragment를 관리하는게
  • Pyxis: 관점에선 맞아보이는데
  • Zeallat: 일단 뭐 그런 케이스라면
  • Zeallat: parent fragment 죽으면
  • Zeallat: child도 다 죽어야죠
  • Pyxis: 그렇겠져
  • Zeallat: scope측면에서
  • Zeallat: parent에 종속적이여야하죠
  • Pyxis: 그렇죠
  • Pyxis: 그게 좀 더 하긴 하는데
  • Pyxis: AndroidSupportInjection 쓰니까
  • Pyxis: Non injected 로그 뜨면서
  • Pyxis: 인젝트가 안되더라고요
  • Pyxis: parent fragmentㅇ서
  • Pyxis: 에서
  • Pyxis: 여기서 parent fragment인 DialogFragment에 injection이 왜 필요한가
  • Pyxis: 하면
  • Pyxis: scope 의 생성은 HasSupportFragmnetInjector 인터페이스 기반이라
  • Pyxis: 서브컴포넌트도 이 기준으로 생성되고
  • Pyxis: 그래서 parent fragment가 child fragment를 가지기 위해서는
  • Pyxis: parent fragment가 HasSupportFragmentInjector를 구현하고
  • Pyxis: DIspatchingAndroidInjector를 반환해야 되는데
  • Pyxis: 구현하는거까진 문제가 없는데
  • Pyxis: DialogFragment의 onCreateView에서 AndroidSupportInjection 하니까
  • Pyxis: 죽더라고요
  • Pyxis: 그래서 일단 임시방편으로 AppComponent에다가
  • Pyxis: inject 메서드 만들어놓고
  • Pyxis: 그걸 호출하는 식으로 강제 인젝트는 했는데
  • Pyxis: 결론적으로
  • Pyxis: 이 접근방법이 맞는건지: 그리고 이 경우에 dialogfragment에 정상적으로 inject하는 방법이 어떤건지
  • Pyxis: 이 두개가 문제였어요
  • Pyxis: 일단 dagger-android 자체는 DialogFragment를 정식적으로 지원하고는 있어요
  • Pyxis: https://github.com/google/dagger/blob/master/java/dagger/android/DaggerDialogFragment.java
  • Pyxis: ?
  • Pyxis: 잠만
  • Pyxis: 이 링크가 답이네요
  • Pyxis: ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
  • Zeallat: 휴 오늘도 제가 Pyxis님 고민을 한건 해결해드렸군요
  • Zeallat: Pyxis님 저 없으면 어떻게 사시나요
  • Pyxis: ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ
  • Zeallat: 하하
  • Pyxis: 혼자 막 정리해보니
  • Pyxis: 답이 나옴
  • Zeallat: 그래서 답이뭔가요?
  • Zeallat: 그냥 저 링크식으로
  • Pyxis: 네
  • Zeallat: android injection
  • Zeallat: 하면되나여
  • Pyxis: onAttach에서
  • Pyxis: 해주면 되는거같아요]
  • Zeallat: onCreateView가 아니고
  • Zeallat: onAttatch가 해담인가요
  • Zeallat: ㅋㅋ
  • Pyxis: 뭐 그러면 복잡하게 할 필요 없이
  • Pyxis: DialogFragment에다가 InjectFragmnet달아놓고
  • Pyxis: onAttach에다가 걸면 되겠네여
  • Pyxis: 담 버전부터 대응해봐야될듯