Spring Boot アプリケーションをAWS EKSにデプロイ

Spring BootアプリケーションをAWS EKSにデプロイして、
LoadBalancer タイプのサービスで外部公開するまでの手順、備忘録

spring boot アプリケーションの作成

Docker で Spring Bootを参考にspring bootアプリケーションの実行可能jarファイルを作成

Docker image構築

事前にDocker実行環境を作成しておくこと
Install Docker Desktop on Windows | Docker Documentation

Dockerfileを作成し、イメージをbuild

$ cat Dockerfile
FROM openjdk:8-jdk-alpine AS builder
WORKDIR target/dependency
ARG APPJAR=build/libs/*.jar
COPY ${APPJAR} app.jar
RUN jar -xf ./app.jar

FROM openjdk:8-jre-alpine
VOLUME /tmp
ARG DEPENDENCY=target/dependency
COPY --from=builder ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=builder ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=builder ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","com.example.demo.DemoApplication"]


$ docker build -t test .

ImageをECRにPush

事前にAWS CLIで以下コマンドやeksctlが利用できる環境を作成しておくこと
eksctl の開始方法 - Amazon EKS

リポジトリを作成

aws ecr create-repository \
    --repository-name test \
    --image-scanning-configuration scanOnPush=true \
    --region ap-northeast-1 

ログイン

aws ecr get-login-password | docker login --username AWS --password-stdin  xxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com

イメージのタグ付け

docker tag test:latest  xxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/test:latest

push

docker push  xxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/test:latest

EKS clusterの作成

eksctlコマンドで実施
今回は、t3.microインスタンスタイプのlinuxのみのワークロードクラスターで作成

eksctl create cluster \
--name prod \
--version 1.16 \
--region ap-northeast-1 \
--nodegroup-name standard-workers \
--node-type t3.micro \
--nodes 2 
--nodes-min 2 \
--nodes-max 2 \
--ssh-access \
--ssh-public-key <SSH接続時の公開鍵> \
--managed

spring boot アプリケーションのデプロイメント定義を作成

kubectl create deployment test --image= xxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/test --dry-run -o=yaml > deployment.yaml

デプロイメントの適用

kubectl apply -f deployment.yaml

作成されていることを確認

$ kubectl get all
NAME                        READY   STATUS    RESTARTS   AGE
pod/test-55cfbd7855-frrx8   1/1     Running   0          5s

外部接続のためのロードバランサー サービス定義を作成

$ cat  loadbalancer.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: LoadBalancer
  selector:
    app: test
  ports:
    - protocol: TCP
      port: 8080
      targetPort: 8080

serviceを適用

kubectl create -f loadbalancer.yaml

作成されていることを確認

$ kubectl get service/nginx-service
NAME            TYPE           CLUSTER-IP       EXTERNAL-IP                                                                   PORT(S)          AGE
nginx-service   LoadBalancer   yy.yyy.yyy.yyy  xxxxxxxxx.ap-northeast-1.elb.amazonaws.com   8080:31880/TCP   24s

アクセス!

f:id:genepon:20200620212649p:plain
access_result

参考

Docker で Spring Boot
Spring Boot Kubernetes
AWS CLI を使用した Amazon ECR の開始方法

EC2の自動停止スクリプト(Lambda+CloudWatch Events)

うはww また消し忘れて寝てるww 請求書((((;゚Д゚))))
となるのを避けるのを防ぐためのスクリプトのお話。 今回は、下記で実施
- Lambda
- bash
- awscli
- CloudWatch Events

前提

  • 開発環境でaws cliが設定済み
  • Lambdaで利用するロールは事前に作成済みとする
    今回の場合だと、AWSLambdaBasicExecutionRole以外にも、
    EC2を操作する権限や、パラメータストアから値を取得する権限が必要

シェルの用意

除外する方法もいろいろ。今回はAWS Systems Manager のパラメータストアを利用
組織とかで使うなら、tagやファイルの読み込みのほうが使いやすそう

#!/bin/bash

#起動中インスタンスの一覧を取得
RUNNING_INSTANCE=$(aws ec2 describe-instances | jq -r '[.Reservations[].Instances[] | select(.State.Name == "running") | .InstanceId] | join(" ")')
#除外対象とするインスタンスの一覧を取得
EXCLUDE_LIST=$(aws ssm get-parameter --name <設定するパラメータキー名> | jq -r '.Parameter.Value' | sed -e 's/,/ /g' )

#除外対象以外のインスタンスを停止
for e in ${RUNNING_INSTANCE[@]}; do
    if ! `echo ${EXCLUDE_LIST[@]} | grep -q "${e[@]}"` ; then
    aws ec2 stop-instances --instance-ids ${e[@]}
else
    :
fi
done

パラメータストアの設定

valueに除外対象のインスタンスIdをカンマ区切りで記載

aws ssm put-parameter \
    --name "パラメータキー名" \
    --value "除外対象のインスタンスID_1,除外対象のインスタンスID_2" \
    --type StringList

Lamda関数の作成

bashの場合カスタムランタイムで起動する。すぐに終わるかと思ったが、 awsチュートリアルで進めると、awscliやjqコマンドをたたく際に、そんなコマンドねーぞと怒られるので少し準備が必要。 Bashを動かす環境としてbash-lambda-layerなる便利なものが公開されていたのでこちらをlayerとして使わせていただきます<(_ _)>

アップロードするスクリプトファイルの作成

bash-lambda-layer example-basic.shを参考に用意したシェルを修正し、index.shとして保存

function handler () {
    set -e

    #起動中インスタンスの一覧を取得
    RUNNING_INSTANCE=$(aws ec2 describe-instances | jq -r '[.Reservations[].Instances[] | select(.State.Name == "running") | .InstanceId] | join(" ")')
    #除外対象とするインスタンスの一覧を取得
    EXCLUDE_LIST=$(aws ssm get-parameter --name exclude.ec2instance | jq -r '.Parameter.Value' | sed -e 's/,/ /g' )
    
    #除外対象以外のインスタンスを停止
    for e in ${RUNNING_INSTANCE[@]}; do
        if ! `echo ${EXCLUDE_LIST[@]} | grep -q "${e[@]}"` ; then
        aws ec2 stop-instances --instance-ids ${e[@]}
    else
        :
    fi
    done

    echo "{\"success\": true}" >&2
}

アップロードのためindex.shをzip化

 zip function.zip index.sh

awscliでlambda関数の作成

$ aws lambda create-function \
    --function-name terminate-ec2 \
    --role <事前準備済みのroleを指定> \
    --handler index.handler \
    --runtime provided \
    --timeout --> defaultの3秒は厳しいので適当な値を設定 \
    --memory-size --> memoryもデフォルト128 MBだと結構時間がかかるのでry \
    --layers arn:aws:lambda:ap-northeast-1:744348701589:layer:bash:8 \
    --zip-file fileb://function.zip

起動確認

$ aws lambda invoke --function-name terminate-ec2 response.txt
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
$ cat response.txt
{"success": true}

Cloudwatch Eventsの作成

時刻はUTCなので日本時間では+9 (夜中の3時に実行)

$ aws events put-rule --name "Ec2TerminateAtMidnight" --schedule-expression "cron(0 18 * * ? *)" --state ENABLED

CloudWatch Eventsから作成したLamba実行できるための権限付与

$ aws lambda add-permission \
  --function-name "terminate-ec2" \
  --statement-id "cwevents" \
  --action 'lambda:InvokeFunction' \
  --principal events.amazonaws.com \
  --source-arn arn:aws:events:ap-northeast-1:<accountId>:rule/Ec2TerminateAtMidnight

targetの設定 (eventとlambda関数の紐づけ)

aws events put-targets --rule "Ec2TerminateAtMidnight" --targets Arn=arn:aws:lambda:ap-northeast-1:<accountId>:function:terminate-ec2,Id=1

こんな感じでできれば完成

f:id:genepon:20200613232226p:plain
完成図

参考

チュートリアル – カスタムランタイムの公開
gkrizek/bash-lambda-layer
AWS CLIを使ってEC2インスタンスの情報を取得する

log4j2を使用したwebアプリでのファイルへのログ出力

Webアプリケーションでログの制御をlog4j2を使用して行う方法についてのメモ。
以下はEclipseを使って作成した手順です。

環境

手順

1. jarファイルの用意

公式ページに行って、Log4j 2 のjarファイルを取得する。
Log4j 2.3はJavaの6がサポートされる最後のバージョンで以下のjarファイルが必要。

2.設定ファイル(log4j2.xml)について

設定ファイルがない場合は、デフォルト設定でのログ出力が行われる。
デフォルト設定を設定ファイルで記述した場合以下の記載となる。

<?xml version="1.0" encoding="UTF-8"?>
  <Configuration status="WARN">
    <Appenders>
      <Console name="Console" target="SYSTEM_OUT">
        <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
       </Console>
      </Appenders>
      <Loggers>
        <Root level="error">
          <AppenderRef ref="Console"/>
        </Root>
      </Loggers>
    </Configuration>

Configuration
statusはlog4j2自体の内部ログの出力レベル設定。status="off"で出力停止

Appenders
どのようにLogEventを配信するかを定義する。
Console appenderは System.out または System.errへの出力
targetで標準出力か標準エラー出力かを定義する。

PatternLayout
出力形式の設定。細かな設定値はマニュアルに記載参照。 デフォルトのpatternの記載内容は以下

%d{HH:mm:ss.SSS} -> 時刻形式。%d{}で時刻形式を囲んで設定
%t               ->  スレッド名
%msg             ->  ログメッセージ
hh:mm:ss.SSS [threadName] [logLevel] [loggerName]-[msg]のような形式で出力される

Loggers
Javaパッケージ毎に出力するログレベルやログの出力方法(Appender)を指定する。
設定ファイルがない場合、またはLogger name="hoge"の形でロガー名を指定しない場合は
Rootの設定を適用。 level="error"であればERROR以上のレベル(ERROR,FATAL)で出力を行う。
AppenderRef refで指定する名称にはAppenderのnameを使用する。

仮に、

<Loggers>
    <Root level="debug" ・・・>
    ・・・・
    <Logger name="X"  level="error"  >
    ・・・・

の設定が存在した場合、下記のJavaパッケージは以下のレベルで出力を行う

X --> Xが適用 ERROR,FATAL
X.Y --> Xが適用 ERROR,FATAL
Z --> Rootが適用 DEBUG,INFO,WARN,ERROR,FATAL

3.ファイル出力のための設定ファイル(log4j2.xml)の準備

今回は、ファイル出力を行うため、RollingFileAppenderを用いて以下に設定

<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="off">
<Appenders>
<RollingFile name="RollingFile" fileName="D:/testlogs/test.log"
     filePattern="D:/testlogs/test-%d{yyyy-MM-dd}-%i.zip">
        <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger - %msg%n"/>
          <Policies>
              <OnStartupTriggeringPolicy />
              <SizeBasedTriggeringPolicy size="20 MB" />
              <TimeBasedTriggeringPolicy />
          </Policies>
          <DefaultRolloverStrategy max="20"/>
        </RollingFile>
      </Appenders>
      <Loggers>
        <Root level="error">
          <AppenderRef ref="RollingFile"/>
        </Root>  
      </Loggers>
    </Configuration>

ログ出力先/名称
D:/testlogs/test.log
フルパス、相対パスどちらの指定も可能

ログローテート条件
以下の条件で新たなファイルを生成し、ログローテートを実施
- ログの開始日と日付が異なる場合
- 新たなJVMが起動した場合
- ファイルサイズが20MBを超えた場合

ローテートファイル形式
test-%d{yyyy-MM-dd}-%iの名称でzip圧縮(%iは連番でmaxは20)

4.Tomcatプロジェクトの作成

Eclipseのメニュー -> ファイル -> 新規 -> プロジェクト -> Tomcatプロジェクトを選択

プロジェクト名は任意の名前を入力 f:id:genepon:20190121224924p:plain

5.jarの配置と設定ファイル(log4j2.xml)の配置

WEB-INF/lib以下のファイルにダウンロードしたJarファイルを配置する。 また、log4j2.xmlをsrc直下に配置する。 配置したらeclipse上のプロジェクトフォルダ上で右クリックして更新。 jarファイルがパッケージ・エクスプローラーに表示されたら、 ファイルを選択して右クリック->ビルドパスへ追加。

f:id:genepon:20190121224926p:plain

6.ログを出力するためのサーブレットクラスの作成

「WEB_INF/src」フォルダの上で右クリック、新規 -> クラス を選択し、適当なパッケージ名とクラス名を入力 f:id:genepon:20190121224931p:plain

サンプルコード(LogOut.java)
package foo;
import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;


public class LogOut extends HttpServlet {
    static Logger logger = LogManager.getLogger(LogOut.class.getName());
     
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
               throws ServletException, IOException {
        
        logger.trace("Logged by logger.trace");
        logger.debug("Logged by logger.debug");
        logger.info ("Logged by logger.info");
        logger.warn ("Logged by logger.trace");
        logger.error("Logged by logger.error");
        logger.fatal("Logged by logger.fatal");
    }   
}

7.web.xmlの作成

WEB_INF/src/フォルダの上で右クリック、新規 -> ファイル、ファイル名をweb.xmlとして作成

<?xml version="1.0" encoding="ISO-8859-1"?>

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    version="2.5">
    
    <servlet>
        <servlet-name>LogOut</servlet-name>
        <servlet-class>foo.LogOut</servlet-class>
    </servlet>
    
    <servlet-mapping>
        <servlet-name>LogOut</servlet-name>
        <url-pattern>/LogOut</url-pattern>
    </servlet-mapping>
    
</web-app>

8.ブラウザでアクセスして確認

設定したERRORとFATALレベルのログがD:\testlogs\test.logに出力されていることを確認

23:06:02.434 [http-8080-1] ERROR foo.LogOut - Logged by logger.error
23:06:02.437 [http-8080-1] FATAL foo.LogOut - Logged by logger.fatal

log4j2.xmlPoliciesOnStartupTriggeringPolicyを設定しているためTomcatの再起動で、 ログがローテートされ圧縮される

f:id:genepon:20190121225004p:plain

参考

Log4j – Overview - Apache Log4j 2

Centos 6への Tomcat7のインストール

Tomcatのおさらい

[Q] そもそも tomcatって?
[A] サーブレットコンテナ

[Q] サーブレットコンテナとは?
[A]サーブレットを呼び出して、実行する環境

[Q] 代表的なサーブレットコンテナって?
[A] よく聞くのは、TomcatGlassfish、Jetty...とか

[Q] サーブレットって?
[A] サーバー上で動作するJavaプログラム

[Q] サーブレットの役割って?
[A] 三層アーキテクチャのプレゼンテーション層、MVCの構造のController

インストール方法

yumのようなパッケージ管理コマンドを使ってインストールしたり、
公式サイトから、インストーラやバイナリの圧縮ファイルを取得し展開しても可。お好みで。
本番環境などで、脆弱性対応による、早急なupdateなどが発生する場合には、
リポジトリに対応する最新版がないこともあるので、後者のほうが楽と思われる。

tar.gzを使用してCentOSへインストールする場合の方法について、記載するが
大きく必要な手順としては

  1. JAVAのインストール
  2. 環境変数の設定
  3. Tomcat起動ユーザーの作成(セキュリティホール存在時にroot権限が乗っ取られるのを避けるため)
  4. Tomcatの展開

運用する際には当然他にも細かな設定がいろいろと必要になってくる。

環境

インストール

ランタイムと開発環境のインストール

# yum install java-1.7.0-openjdk
# yum install java-1.7.0-openjdk-devel
# java  -version
java version "1.7.0_201"
OpenJDK Runtime Environment (rhel-2.6.16.0.el6_10-x86_64 u201-b00)
OpenJDK 64-Bit Server VM (build 24.201-b00, mixed mode
# javac -version
javac 1.7.0_201

Tomcat を実行する専用ユーザーの作成

# useradd -s /sbin/nologin tomcat

バイナリをwgetで取得 (v7.0.92)

# wget https://www-eu.apache.org/dist/tomcat/tomcat-7/v7.0.92/bin/apache-tomcat-7.0.92.tar.gz

取得したtar.gzを展開

# tar -xf ~/apache-tomcat-7.0.92.tar.gz
# mv ~/apache-tomcat-7.0.92 ~/tomcat-7.0.92
# mkdir /opt/tomcat
# mv ~/tomcat-7.0.92/ /opt/tomcat
# chown -R tomcat:tomcat /opt/tomcat/

環境変数の設定

# vi /etc/profile

#下記の記載を追加
JRE_HOME=/usr/lib/jvm/jre
CATALINA_HOME=/opt/tomcat/tomcat-7.0.92
export JRE_HOME CATALINA_HOME

/etc/profileの設定を反映

# source /etc/profile
# echo $JRE_HOME $CATALINA_HOME

tomcatの軌道/停止

# sudo -u tomcat /opt/tomcat/tomcat-7.0.92/bin/startup.sh
# sudo -u tomcat /opt/tomcat/tomcat-7.0.92/bin/shutdown.sh

あとは、ブラウザでインストールしたホスト名(ip):8080にアクセスすれば、
おなじみの猫の画面が見えるはず。いつも思うがこの猫あまりかわいくない。

シェルで2つのcsvファイルを一行ずつ読み込み結合

大量のレコードに対し、2つのcsvファイルをシェルで結合する方法。
やりたかったのは、right.csvの各行に対して、left.csvの組み合わせを結合し、
出力ファイル(output.csv)を生成すること。

left.csv

number,name
01,hoge
02,fuga
03,hogehoge
04,fugafuga

right.csv

taskNumber,taskName
100,eat
200,sleep
300,walk
400,work

output.csv

number,name,taskNumber,taskName
01,hoge,100,eat
01,hoge,200,sleep
01,hoge,300,walk
01,hoge,400,work
(省略)
.
.
04,fugafuga,400,work

ループ処理で1行ずつテキストを読み込む構文

while read ブロック変数
do
  処理
done < 読み込みファイル

サンプルスクリプト

#!/bin/bash

#出力ファイルの存在チェック
#存在していれば削除
if [ -e output.csv ]; then
  rm -f output.csv
fi

#headコマンドでheaderのみを取得
#結合してoutput.csvへ出力
leftHeader=$(head -n 1 left.csv)
rightHeader=$(head -n 1 right.csv)

echo "${leftHeader},${rightHeader}" >> output.csv

#headerを除いて一行ずつループで取得
#結合してoutput.csvへ出力
tail -n +2 left.csv | while read line1
do
  number=`echo ${line1} | cut -d , -f 1`
  name=`echo ${line1} | cut -d , -f 2`

  tail -n +2 right.csv | while read line2
  do
    taskNumber=`echo ${line2} | cut -d , -f 1`
    taskName=`echo ${line2} | cut -d , -f 2`
    echo "${number},${name},${taskNumber},${taskName}" >> output.csv
  done
done

スクリプト書いたら、実行して気長に待つ。。

Android emulatorでtcpdumpを取得

社内プロキシを通すためにAndroidのパケットログを取得する必要があり、
方法を調べたので忘れないようにまとめておく。

web proxyであればFiddlerを使うなどいろいろな方法があると思うが、
最も手っ取り早いのはemulatorでtcpdumpを取得してWiresharkとかで確認する方法だと思う。
結論から言ってしまうと、下記のコマンドでtcpdumpを取得できる。

emulator -tcpdump <出力ファイルパス> -avd <3.で調べたemulator名>

環境

Android Studio 3.1.3
OS Windows7(64bit)

手順

  1. SDKのインストールディレクトリを確認
    わからなければ、Android Studioを開きTools > SDK Managerから選択してパスを確認。

  2. SDKのインストールディレクトリ配下のemulatorへ移動

  3. 起動するemulatorの名称を下記のコマンドで確認
    emulator -list-avds

  4. 下記のコマンドでemulatorを起動
    emulator -tcpdump <ダンプを出力するファイルパス> -avd <3.で調べたemulator名>

  5. ダンプを取得するための操作をemulatorで実施

  6. emulatorを終了

  7. 取得したファイルをwiresharkとかに取り込んで確認

参考

-Monitoring Android network traffic
-Start the emulator from the command line  |  Android Developers

Excel(VBA)でOracleとの連携(oo4o)

ExcelOracle Databaseからデータを取得する機会があり、Oracleに詳しくないこともあり、 VBAexcelに接続する方法を調べたので纏めておく。

主要な方法としては3つあるらしい

  • ActiveXData Objects(以下ADO)からODBCドライバを使用する方法
  • ADOからOLE DBドライバを使用する方法
  • Oracle Objectsfor OLE(以下oo4o)を使用する方法

今回はoo4oを使用する方法についてまとめる。

環境

Excel 2010(32bit)
OS Windows7(64bit)
Oracle Oracle Database Standard Edition One 11.2.0.4.v16

手順

  1. 公式サイトに行って、ODACパッケージをダウンロード。
    http://www.oracle.com/technetwork/jp/database/windows/downloads/utilsoft-087491-ja.html
    パッケージの中に含まれているOracle Objects for OLE(OO4O)を利用する。

  2. ODACパッケージを任意のディレクトリへ展開
    展開先のディレクトリに存在するinstall.batを起動して、OO4Oをインストール
    コマンド実行例

  3. 再起動 OO4Oインストールはレジストリ登録を伴うため、VBA使用前に念のため再起動

  4. ExcelVBAプロジェクトの参照可能なライブラリファイルの追加
    ツールから参照設定、Oracle InProc Server 5.0 Type LibraryにチェックしてOK

  5. VBAでマクロを作成

VBA サンプル

下記はOracleに接続して、ホスト名を取得し、セルに出力するサンプル

Sub Sample()
 
    'セッション
    Dim OracleSession As Object
    'データベース
    Dim OracleDB As Object
    'SQL
    Dim sqlString As String
    
    'ホスト名を取得
    sqlString = " SELECT HOST_NAME FROM V$INSTANCE"
    

    'セッションオブジェクト作成(OO4O)
    Set OracleSession = CreateObject("OracleInProcServer.XOraSession")
    'tns名 user pass を指定
    Set OracleDB = OracleSession.OpenDatabase("<tns>", "<user>" & "/" & "<pass>", 0&)
    
    ' 検索実行
    Set objRe = OracleDB.CreateDynaset(sqlString, 0&)

    ' 0件チェック
    If objRe.EOF = False Then
        Dim i As Integer
        ' ホスト名のフィールドに検索結果が存在すればシートのA列1行目に取得したホスト名を出力
        If Not IsNull(objRe.Fields("HOST_NAME").Value) Then
                Cells(1, "A").Value = objRe.Fields("HOST_NAME").Value
        End If
    End If

    '各オブジェクト開放
    objRe.Close
    Set objRe = Nothing
    OracleDB.Close
    Set OracleDB = Nothing
    Set OracleSession = Nothing
    
End Sub

コマンド実行例

(展開ディレクトリ) > install.bat oo4o c:\oracle orahome true

引数1: 個別にインストールするコンポーネント
引数2: ORACLE_HOME パス
引数3: ORACLE_HOME 名
引数4: 依存性のあるコンポーネントを合わせてインストールするかどうか

参考