はじめに
前回の記事 でGithubActionsを使ってBeanstalkへのデプロイが簡単に行えることがわかりました。
今回は実際に社内で運用しているEC2にデプロイできるworkflowを書いてみます。
せっかくなので、インスタンス作成直後のまっさらな状態のEC2に対しても、自動でセットアップしてデプロイできるようにしたいと思います。
前提
- パブリックサブネットに配置したEC2があること
- EC2にSSHできる秘密鍵があること
- Githubのアカウントがあること
開発環境
macOS Catalina
OpenJDK 1.8
SpringBoot 2.2.6
Gradle 6.3
#大まかな作業の流れ
- デプロイ用のSpringBootアプリを用意する
- RepositoryのSecretsに秘密鍵を登録する
- workflowを書く
- pushして自動デプロイを確認
1. デプロイ用のSpringBootアプリを用意する
まずはデプロイして動作確認するためのSpringBootのアプリを作成します。
例によってサクっと spring initializr で GradleProject に lombok, Spring Web をdependenciesに追加して作成 します。
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
に追加します。
bootJar {
launchScript()
}
1-2. Controllerを作成
動作確認用の簡単なControllerを作成しておきます。
@RestController
public class DemoController {
@GetMapping("/demo")
public String demo() {
return "Hello Actions demo!!";
}
}
1-3. ポート番号80で待ち受け
EC2でHTTP通信を待ち受けるため application.properties
を編集してポート80を設定します。
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
にアクセスしてみます。
上のイメージのように、先ほど作成したControllerのレスポンスが表示されていれば、アプリの準備は完了です。
#2. RepositoryのSecretsに秘密鍵を登録する
作成したプロジェクトをGithubにプッシュしてリポジトリを作成し、いよいよ workflow
の作成を行います。
その前に、この workflow
ではEC2へSSH接続を行うため、秘密鍵情報を Secrets
に登録しておきます。
今回は AWS_EC2_PRIVATE_KEY
という名前で秘密鍵情報を登録します。
EC2インスタンス作成時に作った 〜.pem
ファイル、または ssh-keygen
などで生成したキーペアの秘密鍵ファイルの中身のテキストをまるっとコピーして貼り付けます。
3. workflowを書く
EC2へのデプロイのため、普段手作業でSCPとSSHでjarファイルのコピー&サービスの(再)起動を行なっていたものを workflow
化しました。
やってることは、EC2に
-
java
をインストール -
/var/apps/(アプリ名)/(アプリ名).jar
と(アプリ名).conf
ファイルを配置 -
/etc/systemd/system/(アプリ名).service
ファイルを配置 -
sudo systemctl start(or restart) (アプリ名)
でサービス起動
です。
出来上がった workflow
ファイルは以下になります。
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_HOST
と APP_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
ファイルを ssh
と scp
でEC2サーバーにコピーします。
ここで先ほど登録した秘密鍵情報 AWS_EC2_PRIVATE_KEY
からファイルを復元して ssh
と scp
の秘密鍵ファイルとして指定します。
- 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
この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タブから実行中のタスクが確認できます。
このように全て✅がついていれば正常に完了です。
念の為、SSHのセットアップ状況のログもみてみましょう。
SSH EC2 Setup and Deploy
のStepの詳細を見ると、Javaのインストール、 conf
service
ファイルの作成、アプリの起動まで成功していました。
EC2にブラウザからアクセスして動作確認
無事にデプロイが完了したので、ブラウザから
http://(ec2のパブリックIP)/demo
にアクセスしてみます。
まっさらなEC2に、無事に初回セットアップ&デプロイが行えました!
アプリの再リリースも行えるか動作確認
初回デプロイだけでなく、再デプロイが行えることも確認するために、Controllerの一部の文言を変更して再度Commit&Pushしてみます。
@RestController
public class DemoController {
@GetMapping("/demo")
public String demo() {
return "Hello CI/CD demo!!";
}
}
再デプロイ時のSSHのセットアップ状況を見ると、期待通りに already ...
でSkipされ、アプリの restart
が行われています。
無事に再デプロイされ、アプリの変更が反映されました!
まとめ
workflow
でSSHを使うことで、EC2への自動デプロイも簡単に行えるようになりました。
特に、サードパーティの ssh-remote-commands は今後も重宝しそうです。
参考
今回の workflow
作成に辺り、以下の記事を参考にさせていただきました。