LoginSignup
10
13

More than 3 years have passed since last update.

SpringBootアプリをGithubActionsでEC2にデプロイしてみた

Last updated at Posted at 2020-06-01

はじめに

前回の記事 でGithubActionsを使ってBeanstalkへのデプロイが簡単に行えることがわかりました。
今回は実際に社内で運用しているEC2にデプロイできるworkflowを書いてみます。

せっかくなので、インスタンス作成直後のまっさらな状態のEC2に対しても、自動でセットアップしてデプロイできるようにしたいと思います。

前提

  • パブリックサブネットに配置したEC2があること
  • EC2にSSHできる秘密鍵があること
  • Githubのアカウントがあること

開発環境

macOS Catalina
OpenJDK 1.8
SpringBoot 2.2.6
Gradle 6.3

大まかな作業の流れ

  1. デプロイ用のSpringBootアプリを用意する
  2. RepositoryのSecretsに秘密鍵を登録する
  3. workflowを書く
  4. pushして自動デプロイを確認

1. デプロイ用のSpringBootアプリを用意する

まずはデプロイして動作確認するためのSpringBootのアプリを作成します。
例によってサクっと spring initializr で GradleProject に lombok, Spring Web をdependenciesに追加して作成 します。

build.gradle
plugins {
    id 'org.springframework.boot' version '2.2.6.RELEASE'
    id 'io.spring.dependency-management' version '1.0.9.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

test {
    useJUnitPlatform()
}

このプロジェクトに対し、次の変更を加えていきます。

1-1. 完全な実行可能Jarのビルド設定を build.gradle に追加

デプロイするアプリをサービスとして実行するため、完全に実行可能なJarを生成 するようにします。
以下の bootJar タスクを build.gradle に追加します。

build.gradle
bootJar {
    launchScript()
}

1-2. Controllerを作成

動作確認用の簡単なControllerを作成しておきます。

DemoController.java

@RestController
public class DemoController {
    @GetMapping("/demo")
    public String demo() {
        return "Hello Actions demo!!";
    }
}

1-3. ポート番号80で待ち受け

EC2でHTTP通信を待ち受けるため application.properties を編集してポート80を設定します。

application.properties
server.port=80

以上でアプリの準備は完了です。

1-4. アプリの動作確認

EC2にデプロイする前に、アプリが正しく動作することを確認しておきます。
以下のコマンドで jar をビルドします。

$ ./gradlew build

すると、 build/libs 内にjarが生成されるので、ローカルでアプリを起動してみます。
完全に実行可能なJar で作成しているため、jarを直接叩いて実行できることを確認しておきます。

$ ./build/libs/actions-0.0.1-SNAPSHOT.jar

起動後、ブラウザで http://localhost/demo にアクセスしてみます。

image.png

上のイメージのように、先ほど作成したControllerのレスポンスが表示されていれば、アプリの準備は完了です。

2. RepositoryのSecretsに秘密鍵を登録する

作成したプロジェクトをGithubにプッシュしてリポジトリを作成し、いよいよ workflow の作成を行います。
その前に、この workflow ではEC2へSSH接続を行うため、秘密鍵情報を Secrets に登録しておきます。

今回は AWS_EC2_PRIVATE_KEY という名前で秘密鍵情報を登録します。
EC2インスタンス作成時に作った 〜.pem ファイル、または ssh-keygen などで生成したキーペアの秘密鍵ファイルの中身のテキストをまるっとコピーして貼り付けます。
スクリーンショット 2020-06-01 2.16.02.png

3. workflowを書く

EC2へのデプロイのため、普段手作業でSCPとSSHでjarファイルのコピー&サービスの(再)起動を行なっていたものを workflow 化しました。

やってることは、EC2に

  • java をインストール
  • /var/apps/(アプリ名)/(アプリ名).jar(アプリ名).conf ファイルを配置
  • /etc/systemd/system/(アプリ名).service ファイルを配置
  • sudo systemctl start(or restart) (アプリ名) でサービス起動

です。

出来上がった workflow ファイルは以下になります。

.github/workflows/ec2-deploy.yml
name: Gradle build and Deploy to ec2

on:
  push:
    branches: [ master ]

env:
  EC2_USER: 'ec2-user'
  EC2_HOST: (your ec2 public ip address.)
  SRC_PATH: 'build/libs/*.jar'
  DEST_DIR: '/var/apps'

  APP_NAME: 'actions'    # your app name
  JAVA_VERSION: '1.8'    # '1.8' or '11'
  JAVA_OPTS: '-Xms1024M -Xmx1024M'
  RUN_ARGS: '--spring.profiles.active=prod'

jobs:
  deploy:
    name: Gradle build and Deploy to ec2
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Set up JDK.
      uses: actions/setup-java@v1
      with:
        java-version: ${{ env.JAVA_VERSION }}

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - name: Build with Gradle
      run: ./gradlew build

    - name: SCP EC2 Copy app file
      env:
        PRIVATE_KEY: ${{ secrets.AWS_EC2_PRIVATE_KEY }}
      run: |
        echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
        ssh -t -o StrictHostKeyChecking=no -i private_key ${EC2_USER}@${EC2_HOST} "sudo mkdir -p $DEST_DIR/$APP_NAME && sudo chmod -R 777 $DEST_DIR/$APP_NAME"
        scp -i private_key ${SRC_PATH} ${EC2_USER}@${EC2_HOST}:${DEST_DIR}/${APP_NAME}/${APP_NAME}.jar

    - name: SSH EC2 Setup and Deploy
      uses: appleboy/ssh-action@v0.0.9
      with:
        key: ${{ secrets.AWS_EC2_PRIVATE_KEY }}
        username: ${{ env.EC2_USER }}
        host: ${{ env.EC2_HOST }}
        envs: DEST_DIR,JAVA_VERSION,APP_NAME,JAVA_OPTS,RUN_ARGS
        script: |
          echo "===== yum update ====="
          sudo yum update -y
          echo "===== check java install ====="
          if java -version 2>&1 >/dev/null | grep "java version\|openjdk version" ; then
            echo "already installed java."
          else
            echo "install java."
            TARGET=$(yum search java | grep "$JAVA_VERSION.*devel\.\|$JAVA_VERSION.*amazon-corretto\." | awk '{print $1}')
            echo "install target name -> $TARGET"
            sudo yum install -y ${TARGET}
            echo "JAVA_HOME=$(readlink -f /usr/bin/java | sed "s:bin/java::")" | sudo tee -a   /etc/profile
            source /etc/profile
          fi
          echo "===== check conf file ====="
          if [ -f ${DEST_DIR}/${APP_NAME}/${APP_NAME}.conf ]; then
          echo "already exist conf file for $APP_NAME"
          else
          echo "create conf file for $APP_NAME"
          cat <<EOL | sudo tee -a ${DEST_DIR}/${APP_NAME}/${APP_NAME}.conf
          export LANG="ja_JP.UTF8"
          JAVA_OPTS="$JAVA_OPTS"
          RUN_ARGS="$RUN_ARGS"
          EOL
          fi
          echo "===== check exist service file ====="
          if [ -f /etc/systemd/system/${APP_NAME}.service ]; then
          echo "already exist service file for $APP_NAME"
          else
          echo "create service file for $APP_NAME"
          cat <<EOL | sudo tee -a  /etc/systemd/system/${APP_NAME}.service
          [Unit]
          Description = ${APP_NAME} app

          [Service]
          ExecStart =  ${DEST_DIR}/${APP_NAME}/${APP_NAME}.jar
          Restart = always
          Type = simple
          User = root
          Group = root
          SuccessExitStatus = 143

          [Install]
          WantedBy = multi-user.target
          EOL
          fi
          echo "===== application (re)start ====="
          sudo systemctl daemon-reload
          if sudo systemctl status ${APP_NAME} 2>&1 | grep "Active: active (running)" ; then
            echo "${APP_NAME} app restart!!"
            sudo systemctl restart ${APP_NAME}
          else
            echo "${APP_NAME} app start!!"
            sudo systemctl start ${APP_NAME}
          fi

まっさらなEC2にもデプロイできるようにしたため、セットアップ系のSSHのコマンドラインが多いです。
以下に簡単に内容を説明します。

workflowの内容

master ブランチへPush、マージされた場合にこのworkflowが動く様に設定しています。

on:
  push:
    branches: [ master ]

以下は workflow 内で参照する環境変数です。
デプロイするアプリやサーバによって可変な部分を定義しています。


env:
  EC2_USER: 'ec2-user'
  EC2_HOST: (your ec2 public ip address.)
  SRC_PATH: 'build/libs/*.jar'
  DEST_DIR: '/var/apps'

  APP_NAME: 'actions'    # your app name
  JAVA_VERSION: '1.8'    # '1.8' or '11'
  JAVA_OPTS: '-Xms1024M -Xmx1024M'
  RUN_ARGS: '--spring.profiles.active=prod'

EC2_HOSTAPP_NAME はサーバ、アプリ毎に設定を変えて利用します。

定数名 説明
EC2_USER ec2へssh接続する時のユーザ名(ec2-userのままでOK)
EC2_HOST 【要変更】 ec2のパブリックIPアドレス
SRC_PATH ビルドされたjarファイルのパス
DEST_DIR ec2側のアプリを配置するディレクトリのパス
APP_NAME 【要変更】 アプリ名。gradleでビルドされた.jarを ${APP_NAME}.jar にリネームして、ec2の ${DEST_DIR} にコピーする
JAVA_VERSION ビルドや実行する時のJavaのバージョン('1.8'か'11')。EC2にもこのバージョンのjavaがインストールされる。
JAVA_OPTS アプリ起動時のオプション
RUN_ARGS アプリの起動引数。上記例ではSpringのプロファイルを prod に切り替えている

以下は ./gradlew build を実行するまでのStepです。
(ほぼ、公式の雛形 Java with Gradle のままです)


jobs:
  deploy:
    name: Gradle build and Deploy to ec2
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2

    - name: Set up JDK.
      uses: actions/setup-java@v1
      with:
        java-version: ${{ env.JAVA_VERSION }}

    - name: Grant execute permission for gradlew
      run: chmod +x gradlew
    - name: Build with Gradle
      run: ./gradlew build

以下で、ビルドした .jar ファイルを sshscp でEC2サーバーにコピーします。
ここで先ほど登録した秘密鍵情報 AWS_EC2_PRIVATE_KEY からファイルを復元して sshscp の秘密鍵ファイルとして指定します。


    - name: SCP EC2 Copy app file
      env:
        PRIVATE_KEY: ${{ secrets.AWS_EC2_PRIVATE_KEY }}
      run: |
        echo "$PRIVATE_KEY" > private_key && chmod 600 private_key
        ssh -t -o StrictHostKeyChecking=no -i private_key ${EC2_USER}@${EC2_HOST} "sudo mkdir -p $DEST_DIR/$APP_NAME && sudo chmod -R 777 $DEST_DIR/$APP_NAME"
        scp -i private_key ${SRC_PATH} ${EC2_USER}@${EC2_HOST}:${DEST_DIR}/${APP_NAME}/${APP_NAME}.jar

ここまででEC2の /var/apps/(アプリ名)/(アプリ名).jar の配置までが完了します。
以降でEC2の初期セットアップとアプリの起動を行います。

EC2の初期セットアップとアプリのサービス起動まで

workflow で複数行の ssh コマンドを記述可能にする便利なサードバーティ製のActionsがあったので、それを使って ssh コマンドを書いていきます。

https://github.com/marketplace/actions/ssh-remote-commands
image.png

このActionsの script: | 以降にマルチラインのコマンドを記述できるようになります。
注意点として、この script: 内で参照する環境変数は、 envs: に指定してあげる必要があります。

以下は echo ログと sudo yum update を実行しています。


    - name: SSH EC2 Setup and Deploy
      uses: appleboy/ssh-action@v0.0.9
      with:
        key: ${{ secrets.AWS_EC2_PRIVATE_KEY }}
        username: ${{ env.EC2_USER }}
        host: ${{ env.EC2_HOST }}
        envs: DEST_DIR,JAVA_VERSION,APP_NAME,JAVA_OPTS,RUN_ARGS
        script: |
          echo "===== yum update ====="
          sudo yum update -y

以降、セットアップ用のコマンドが続きます。
初回のみ実行するように IF で条件判定を行いつつ、順に・・・

  • Javaのインストール(未インストールの時のみ)
          echo "===== check java install ====="
          if java -version 2>&1 >/dev/null | grep "java version\|openjdk version" ; then
            echo "already installed java."
          else
            echo "install java."
            TARGET=$(yum search java | grep "$JAVA_VERSION.*devel\.\|$JAVA_VERSION.*amazon-corretto\." | awk '{print $1}')
            echo "install target name -> $TARGET"
            sudo yum install -y ${TARGET}
            echo "JAVA_HOME=$(readlink -f /usr/bin/java | sed "s:bin/java::")" | sudo tee -a   /etc/profile
            source /etc/profile
          fi
  • アプリ起動オプション用の (アプリ名).conf ファイルの作成(未作成の時のみ)
          echo "===== check conf file ====="
          if [ -f ${DEST_DIR}/${APP_NAME}/${APP_NAME}.conf ]; then
          echo "already exist conf file for $APP_NAME"
          else
          echo "create conf file for $APP_NAME"
          cat <<EOL | sudo tee -a ${DEST_DIR}/${APP_NAME}/${APP_NAME}.conf
          export LANG="ja_JP.UTF8"
          JAVA_OPTS="$JAVA_OPTS"
          RUN_ARGS="$RUN_ARGS"
          EOL
          fi

↑IF文のインデントがなく読み辛いですが、 cat << EOL ~ EOL の間を .conf ファイルに出力しているためです。

  • アプリのサービス登録用に /etc/systemd/system/(アプリ名).service ファイルの作成(未作成の時のみ)
          echo "===== check exist service file ====="
          if [ -f /etc/systemd/system/${APP_NAME}.service ]; then
          echo "already exist service file for $APP_NAME"
          else
          echo "create service file for $APP_NAME"
          cat <<EOL | sudo tee -a  /etc/systemd/system/${APP_NAME}.service
          [Unit]
          Description = ${APP_NAME} app

          [Service]
          ExecStart =  ${DEST_DIR}/${APP_NAME}/${APP_NAME}.jar
          Restart = always
          Type = simple
          User = root
          Group = root
          SuccessExitStatus = 143

          [Install]
          WantedBy = multi-user.target
          EOL
          fi

以上で、EC2の初期セットアップが完了し、アプリをサービスとして実行できるようになりました。(.conf.service は、予め雛形ファイルを用意しておいて、jarと一緒にアップロードしても良いかもしれません)

最後に、アプリのサービスの(再)起動を行います。

          echo "===== application (re)start ====="
          sudo systemctl daemon-reload
          if sudo systemctl status ${APP_NAME} 2>&1 | grep "Active: active (running)" ; then
            echo "${APP_NAME} app restart!!"
            sudo systemctl restart ${APP_NAME}
          else
            echo "${APP_NAME} app start!!"
            sudo systemctl start ${APP_NAME}
          fi

4. pushして自動デプロイを確認

いよいよアプリの自動デプロイを試してみます。
workflow がプロジェクトの .github/workflows/ec2-deploy.yml にあることを確認して、 master ブランチに対してCommit&Pushしてみましょう。

GithubのプロジェクトのRepositoryのActionsタブから実行中のタスクが確認できます。
image.png

このように全て✅がついていれば正常に完了です。
念の為、SSHのセットアップ状況のログもみてみましょう。
image.png

SSH EC2 Setup and Deploy のStepの詳細を見ると、Javaのインストール、 conf service ファイルの作成、アプリの起動まで成功していました。

EC2にブラウザからアクセスして動作確認

無事にデプロイが完了したので、ブラウザから
http://(ec2のパブリックIP)/demo
にアクセスしてみます。
スクリーンショット 2020-06-01 14.17.27.png

まっさらなEC2に、無事に初回セットアップ&デプロイが行えました!

アプリの再リリースも行えるか動作確認

初回デプロイだけでなく、再デプロイが行えることも確認するために、Controllerの一部の文言を変更して再度Commit&Pushしてみます。

DemoController.java

@RestController
public class DemoController {
    @GetMapping("/demo")
    public String demo() {
        return "Hello CI/CD demo!!";
    }
}

再デプロイ時のSSHのセットアップ状況を見ると、期待通りに already ... でSkipされ、アプリの restart が行われています。

image.png

最後にブラウザをリロードしてみます。
スクリーンショット 2020-06-01 14.36.20.png

無事に再デプロイされ、アプリの変更が反映されました!

まとめ

workflow でSSHを使うことで、EC2への自動デプロイも簡単に行えるようになりました。
特に、サードパーティの ssh-remote-commands は今後も重宝しそうです。

参考

今回の workflow 作成に辺り、以下の記事を参考にさせていただきました。

10
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
13