Docker Image 생성 ( Ubuntu 14.04 + Apache2 + SSL-letsencrypt )

|

Dockerfile을 이용하여 자동화 하여 모든 배포를 끝내려했으나 아래와 같은 이유로 한방 배포가 불가능했습니다.

  • certbot 실행시 입력 커맨드 처리 불가
    • 중간에 Y/N을 입력하는 처리가 나오는데 자동으로 처리 불가
  • apache 자동실행 불가
    • service의 start 커맨드가 불통
    • docker run 실행으로 컨테이너 생성시 FOREGROUD 로 실행하도록 인자값을 추가할 경우 컨테이너가 stop된 이후에 다시 start하면 이미 httpd가 떠있다고 오류 메시지를 뱉으며 실행되지 않는다.

아마 다른 해결책이 있을것 같긴한데 찾지 못해서 위 두가지 문제를 해결하기 위해 다음과 같은 방식으로 생성하였습니다.

Dockerfile build

[Dockerfile]

FROM ubuntu:14.04
RUN apt-get update
RUN apt-get install -y apache2
RUN apt-get install -y software-properties-common
RUN add-apt-repository -y ppa:certbot/certbot
RUN apt-get update
RUN apt-get install -y python-certbot-apache
RUN a2enmod ssl
RUN service apache2 start
RUN cp /etc/apache2/sites-available/default-ssl.conf /etc/apache2/sites-available/[DOMAIN].conf
RUN sed -i 's/\/etc\/ssl\/certs\/ssl-cert-snakeoil.pem/\/etc\/letsencrypt\/live\/[DOMAIN]\/cert.pem/g' /etc/apache2/sites-available/[DOMAIN].conf
RUN sed -i 's/\/etc\/ssl\/private\/ssl-cert-snakeoil.key/\/etc\/letsencrypt\/live\/[DOMAIN]\/privkey.pem/g' /etc/apache2/sites-available/[DOMAIN].conf
RUN sed -i 's/#SSLCertificateChainFile/SSLCertificateChainFile/g' /etc/apache2/sites-available/[DOMAIN].conf
RUN sed -i 's/\/etc\/apache2\/ssl.crt\/server-ca.crt/\/etc\/letsencrypt\/live\/[DOMAIN]\/fullchain.pem/g'  /etc/apache2/sites-available/[DOMAIN].conf

EXPOSE 22 80 443

수작업을 최소화 하기 위해 Dockerfile에서 할 수 있는 모든 작업을 미리 하고 build명령을 통해 image를 생성합니다.

$sudo docker build --tag [REPOSITORY]:[TAG] [Dockerfile PATH]

Container 수작업 후 commit

docker run 커맨드를 통해 컨테이너를 생성하고 추가 작업을 진행합니다.

$sudo docker run -it -p 80:80 -p 443:443 --name [CONTAINER_NAME] [REPOSITORY]:[TAG] /bin/bash

컨테이너 안에서 아래 명령을 실행합니다.

$certbot --apache -d [DOMAIN] -m [E-MAIL] --agree-tos
$a2ensite [DOMAIN]
$service apache2 reload

‘https://[DOMAIN]/’ 에 접속하여 Apache가 정상적으로 뜨는지 확인하고 docker commit 명령을 통해 변경된 Image를 생성합니다.

$sudo docker commit -a [AUTHOR_INFO] -m [MESSAGE] [CONTAINER_NAME] [REPOSITORY]:[TAG]

생성된 이미지를 통해 Docker 실행및 Apache 실행

docker run 커맨드를 통해 컨테이너를 생성하되 Apache 자동실행 옵션은 start/stop 시에도 오류없이 동작하기 위해 추가 아규먼트 없이 기본 생성후 컨테이너를 내리고 실제 컨테이너 부팅시 start 커맨드와 함께 exec 커맨드를 통해 추가로 Apache를 실행할 수 있도록 shell 스크립트 파일을 만들었습니다.

[create-container.sh]
#!/bin/bash
sudo docker run -it -d --name [CONTAINER_NAME] -p 80:80 -p 443:443 [REPOSITORY]:[TAG]
sudo docker stop [CONTAINER_NAME]    
[run-container.sh]
#!/bin/bash
nohup sudo docker start [CONTAINER_NAME] > /dev/null 2>&1
sudo docker exec -d [CONTAINER_NAME] /bin/bash -c '/usr/sbin/apache2ctl -D FOREGROUND'

참고

certbot : https://certbot.eff.org/#ubuntutrusty-apache
[Docker] Container run 이야기 : https://bestna.wordpress.com/2014/11/10/docker-container-run-%EC%9D%B4%EC%95%BC%EA%B8%B0/

하노이탑 알고리즘 Java 샘플

|

재귀호출 알고리즘중 대표격인 하노이탑 알고리즘에 대한 Java 샘플입니다.
하노이탑에 대한 설명은 워낙 많기 때문에 아래 링크로 대체합니다.

하노이탑 - 위키백과

문제 풀이 방식

다양한 원리와 방식에 대한 설명들이 많은데 저는 다음과 같이 단순히 생각해보았습니다.

1

위와 같은 하노이탑중 4번 블럭을 3번째 위치로 옮기기 위해서는 아래와 같이 1,2,3번블럭을 모두 2번째 위치로 옮겨두면 가능합니다.

2

그리고 위 그림처럼 3번 블럭을 2번째 위치로 옮기기 위해서는 1,2,번블럭을 아래와 같이 3번 위치에 옮기면 됩니다.

3

위와 같이 하나의 블럭을 옮기기 위한 원리는 “해당 블럭 위에 있는 블럭을 임시 위치에 두고 해당 블럭을 옮긴다”가 되며 그 원리를 구현한 소스는 아래와 같습니다.

public static void move(Integer target, Stack<Integer> srcStack, Stack<Integer> destStack, Stack<Integer> bufferStack)
{
    if (target == 1)
    {
        moveBlock(srcStack, destStack);
        return;
    }

    move(target-1, srcStack, bufferStack, destStack);
    moveBlock(srcStack, destStack);
    move(target-1, bufferStack, destStack, srcStack);
}

private static void moveBlock(Stack<Integer> srcStack, Stack<Integer> destStack)
{
    destStack.push(srcStack.pop());
}

위 로직을 실행하면 아래와 같이 동작하게 됩니다.

4

전체소스 경로 : (https://github.com/jistol/sample-algorithm/tree/master/hanoi)[https://github.com/jistol/sample-algorithm/tree/master/hanoi]

Markdown syntax highlight

|

기존 Github Page에서 코드라인의 문법 하이라이팅 방법은 아래 방법을 사용했었습니다.

{% highlight java %}
public static void main(String… args)
{
    System.out.println(“Hello World.”)
}
{% endhighlight %}

위 방식은 Jekyll에서 사용할 수 있는 liquid tag 방식으로 이렇게 사용할 경우 Jekyll서버상에서는 예쁘게 변경되어 보이나, 문서 작업시 Atom Editor의 미리보기에서는 아래와 같이 문자열로 보이게 됩니다.

liquid tag

Atom Editor 미리보기에서도 하이라이팅된 코드를 보고 싶을 경우 아래와 같이 코드블럭을 통해 쓸 수 있습니다.

```java
public static void main(String… args)
{
    System.out.println(“Hello World.”)
}
```

another style

사용가능한 하이라이팅 포맷은 c, java, bash, sql, html, js, scala, xml… 등 다양하며 전체 포맷은 Syntax highlighting in markdown 링크를 참고 하시기 바랍니다.

[Spring] Spring Data Rest Respository 소개 및 샘플

|

다른 작업을 하려고 Spring Initializr에서 프로젝트 생성중 “Rest Repository”라는 Dependency항목이 있어 먼가 하고 보다가 서칭한 내용을 정리해보았습니다. 아래 내용에 대한 샘플은 다음 주소에서 확인 할 수 있습니다.

https://github.com/jistol/sample/tree/master/ex-springdata-rest-sample

Spring Data Rest Respository

Spring Data Rest Respository는 Spring Data 프로젝트의 서브 프로젝트로 Repository의 설정만으로 REST API 서버를 구성해주는 신박한 기능입니다. 사용자는 Entity 클래스와 Repository 인터페이스만 작성하면 나머지 CRUD 작업은 모두 알아서 RESTful하게 생성됩니다.

SpringData REST의 주요 기능은 Data Repository로부터 Resource를 추출하는 것으로 핵심은 Repository 인터페이스입니다. 예를 들어 OrderRepository와 같은 Repository인터페이스가 있을 경우 소문자의 복수형 resource를 뽑아내어 /orders 를 만듭니다. 그리고 /orders/{id} 하위에 각 item을 관리할 수 있는 resource를 추출해 냅니다.

시작하기

Spring Initializr페이지에서 아래와 같이 Dependency를 선택하고 “Generate Project”를 눌러 zip으로 다운 받습니다.

project configuration

압축을 풀어 프로젝트의 pom.xml파일을 보면 아래와 같이 Dependency가 포함되 있는 것을 확인 할 수 있습니다.

    <dependencies>
  		<dependency>
  			<groupId>org.springframework.boot</groupId>
  			<artifactId>spring-boot-starter-data-jpa</artifactId>
  		</dependency>
  		<dependency>
  			<groupId>org.springframework.boot</groupId>
  			<artifactId>spring-boot-starter-data-rest</artifactId>
  		</dependency>
  		<dependency>
  			<groupId>org.springframework.data</groupId>
  			<artifactId>spring-data-rest-hal-browser</artifactId>
  		</dependency>

  		<dependency>
  			<groupId>com.h2database</groupId>
  			<artifactId>h2</artifactId>
  			<scope>runtime</scope>
  		</dependency>
  		<dependency>
  			<groupId>org.springframework.boot</groupId>
  			<artifactId>spring-boot-starter-test</artifactId>
  			<scope>test</scope>
  		</dependency>
  	</dependencies>
    

SpringData REST 자체가 어떤 DB를 쓸 지에 대한 설정을 포함하고 있지 않기 때문에 따로 H2 DB를 사용하도록 추가해주었으며 구축된 REST를 쉽게 테스트 해보기 위해 HAL Browser를 추가하였습니다.

먼저 application.properties를 설정합니다.

    # SpringData REST의 기본 context path
    spring.data.rest.basePath=api

    # JPA 설정
    spring.jpa.hibernate.ddl-auto=create
    spring.jpa.show-sql=true

    # H2 DB설정
    spring.datasource.url=jdbc:h2:file:./db/devdb;AUTO_SERVER=TRUE
    spring.datasource.username=test
    spring.datasource.password=test
    spring.datasource.driver-class-name=org.h2.Driver

    spring.h2.console.enabled=true
    spring.h2.console.path=/console
    

Entity는 장바구니(Cart)클래스와 물건(Item)클래스를 만들도록 하겠습니다.

    @Entity
    public class Cart
    {
        @Id
        @GeneratedValue
        private int id;

        private String name;

        private boolean paid;

        @OneToMany(mappedBy = "cart")
        private List<Item> items;

        ....
    }

    @Entity
    public class Item
    {
        @Id
        @GeneratedValue
        private int id;

        private String name;

        private int price;

        @ManyToOne
        @JoinColumn(name = "CART_ID")
        private Cart cart;

        ....
    }
    

그리고 각 Entity의 Repository 인터페이스를 생성합니다. SpringData REST Documentation 사이트에는 CrudRepository를 상속하도록 예제가 나오지만 JpaRepository를 이용해도 무방합니다.

    public interface CartRepository extends JpaRepository<Cart, Integer> {}

    public interface ItemRepository extends JpaRepository<Item, Integer> {}
    

코딩 할 작업은 모두 끝났습니다. 이제 돌려봅시다.

    mvn clean package spring-boot:run
    

project run

실행 로그를 보면 /api로 시작하는 Mapping정보들이 만들어지는것을 볼 수 있습니다. HAL Browser를 통해 실제 Request를 날려봅시다. 아래 URL로 접속합니다.

http://localhost:8080/api

HAL Browser

Explorer에서 직접 주소를 쳐서 호출할 수도 있고 아래 Links를 통해 호출 할 수도 있습니다. Links항목중 Carts의 get버튼을 클릭해보면 현재 Cart목록이 나옵니다.

Cart Empty List

현재는 값이 비어 있는데 Cart값을 하나 넣어보겠습니다. Carts의 non-get버튼을 부르면 Create/Update할 수 있는 화면이 뜹니다.

Cart Insert

다시 Cart목록을 호출해보면 아래와 같이 입력한 Cart가 조회됩니다.

Cart Non-Empty List

/api/{repository}/{id}형태로 단일 목록도 조회 가능합니다. 아래는 Cart의 1번 목록을 조회한 결과 입니다.

Cart 1

그 외의 CRUD 항목도 자동으로 생성하여 제공합니다.

설정

SpringData REST에서 설정 방식은 3가지가 있습니다. 단, Framework가 SpringBoot 1.2 이상 버전일 경우에만 1번 방식을 사용 가능합니다.

  1. application.properties(xml,yaml…)에 설정하기
    spring.data.rest.basePath=/api
    spring.data.rest.defaultPageSize=10
    
  1. @Configuration 사용하기
    @Configuration
    class CustomRestMvcConfiguration {

      @Bean
      public RepositoryRestConfigurer repositoryRestConfigurer() {

        return new RepositoryRestConfigurerAdapter() {

          @Override
          public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
            config.setBasePath("/api");
          }
        };
      }
    }
    
  1. RepositoryRestConfigurerAdapter를 상속받기
    @Component
    public class CustomizedRestMvcConfiguration extends RepositoryRestConfigurerAdapter {

      @Override
      public void configureRepositoryRestConfiguration(RepositoryRestConfiguration config) {
        config.setBasePath("/api");
      }
    }
    

설정 항목은 아래 표를 참고하세요.

Name Description
basePath root URI for Spring Data REST
defaultPageSize change default number of items served in a single page
maxPageSize change maximum number of items in a single page
pageParamName change name of the query parameter for selecting pages
limitParamName change name of the query parameter for number of items to show in a page
sortParamName change name of the query parameter for sorting
defaultMediaType change default media type to use when none is specified
returnBodyOnCreate change if a body should be returned on creating a new entity
returnBodyOnUpdate change if a body should be returned on updating an entity

참고

Spring Data REST - Reference Documentation 2.6.1.RELEASE

Caused by: org.hibernate.AnnotationException: No identifier specified for entity

|

Spring Data JPA + REST 소개블로그 글을 보고 10분만에 REST-API서비스를 만들수 있다는 “spring-data-rest” 샘플 코드를 만들어보던 중에 아래와 같은 오류를 만났습니다.

Caused by: org.hibernate.AnnotationException: No identifier specified for entity: io.github.jistol.Article

Article은 단순한 Entity 클래스로 소스는 아래와 같습니다.

package io.githug.jistol.entity;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.data.annotation.Id;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.OneToMany;
import java.util.Date;
import java.util.List;

@Entity(name = "Article")
public class Article {
    @Id
    @GeneratedValue
    private int id;

    private String nickname;

    @JsonIgnore
    private String password;

    private String content;

    private Date addDate = new Date();

    @OneToMany(mappedBy = "article")
    private List<Comment> comments;

    ....
}

왜 이게 오류가 나지? 라고 생각하며 이것저것 빼먹은게 있나 넣어보고 수정하던 찰라 아주 사소한 실수를 발견했습니다.

import org.springframework.data.annotation.Id;    (X)
import javax.persistence.Id;                      (O)

위와 같이 @Id의 import문이 틀렸던 것입니다. 사소한 부분이지만 생각없이 막 카피하다가 실수하기 딱 좋은 부분이라 메모해둡니다.