20210504のdockerに関する記事は17件です。

dockerで素のAlpine Linuxを使う

※編集途中です(2021-05-03) 環境 依存しないはず。 ちなみに現在は Windows10 Education build 19042.928 Docker Desktop for Windows PowerShell 5.1.19041.906 特段、妙なアップデートはしてないはず。 やってみたこと とにかくコンテナを作る docker run imageを引っ張ってきてコンテナを作る PS> docker run -d -it --name ali alpinelinux /bin/ash docker exec 中に入る docker exec PS> docker exec -it ali /bin/ash 初期設定 setup-ほひゃらら、という便利なスクリプトも用意されています。 https://wiki.alpinelinux.org/wiki/File:Installation-alpine-alpine-setup-4-setup-scripts.png rootパスワードの設定 # export TZ=Asia/Tokyo タイムゾーンの設定 # export TZ=Asia/Tokyo SSHの起動 # export TZ=Asia/Tokyo 常用ユーザの設定 # adduser sudo # # visudo
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

dockerで公式Alpine Linuxイメージを使う

※編集途中です(2021-05-05_02:25) まえがき isoでインストールするといろいろ入ってますが、DockerHubから引っ張ってきたイメージはさらに絞られています。たとえば初期設定に便利なスクリプト"setup-alpine"とか"setup-ほにゃらら"系は全くありません。 (かと思えば、ifconfig や vi は入ってたりします) dockerを動かすだけ and/or イメージをDIYするならubuntuでも贅肉ありありなのでAlpine Linuxでマシンを組みました。 ・・・本当は512MBなVPSのリソースを無駄遣いできない、というビンボー事情です?・・・ 補足: gliderlabs/alpineというのもあり、イメージのサイズは400KBほど小さいです PS > docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE alpine latest 6dbb9cc54074 2 weeks ago 5.61MB gliderlabs/alpine latest 69bbd13383f7 2 years ago 5.19MB が、今日(2021-05-04)時点で2年前から更新されていません。 gliderlabs/alpine:latest # /bin/uname -a Linux *** 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 Linux かといってalpine:latest(2週間前に更新)のカーネルが最新かというと、そうではないようで alpine:latest # /bin/uname -a Linux *** 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 Linux と同じだったりして。ちなみに df の結果も同じなので、わけわかんないです? 原典とか 環境 最近のdockerな環境なら依存しないはず。 ちなみに現在は   Windows10 Education build 19042.928   PowerShell 5.1.19041.906   Docker Desktop for Windows version 20.10.5, build 55c4c88 特段、妙なアップデートはしてないはず。 操作 imageを引っ張ってきてコンテナを作る docker run PS> docker run -d -it -name ali alpine /bin/ash "ali" はコンテナ名です。ご自由につけてください。 /bin/ashは最初から入ってる唯一のshellです。/bin/shでも通じますが同じものが起動します。 docker exec 中に入ります。 PS> docker exec -it ali /bin/ash / # 初期設定 rootパスワードの設定 初期値は"空”です。 # passwd root Changing password for root New password:*** Retype password:*** passwd: password for root changed by root ホスト名の設定 デフォルトではイメージIDになってます。 # hostname aliwww "aliwww"がホスト名です。ご自由にどうぞ。 、、、といきたいところですが、「権限あらへん」エラーとなります。 # echo "aliwww" > /tmp/t # hostname -F /tmp/t も同じエラーとなります。 # echo "aliwww" > /etc/hostname は通りますが再起動すると元のイメージIDに書き戻ります。 現状、docker run する時に"--hostname aliwww"とするしか方法を知りません? タイムゾーンの設定 残念ながら https://wiki.alpinelinux.org/wiki/Setting_the_timezone 、、、と、apk前提です・・・ 基本的なパッケージのインストール apk使用開始前のおまじない なにはともあれ。 # apk update /etc/apk/repositoriesは最初から設定されてますから、よく紹介されている # cat > /etc/apk/repositories << EOF http://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/main http://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/community EOF は要らないです。 タイムゾーンの設定(再掲) 別にUTCでもいいけど。 # apk add tzdata # cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtim # cat "Asia/Tokyo" > /etc/timezone # apk del tzdata SSHのインストールと起動***執筆中*** # apk add openssh # 常用ユーザの設定 となると、rootでsshログインできないから、 # adduser www Changing password for www New password:*** Retype password:*** passwd: password for www changed by root "www"はユーザー名です。ご自由にどうぞ。 sudoのインストールと設定***執筆中*** rootになれないと困るから、 # apk sudo # visudo そのほか IP固定****執筆中*** # cat > /etc/network/interfaces << EOF auto lo iface lo inet loopback EOF docker run する時に"--ip 192.168.0.2"とする方が便利かも。 再起動***執筆中*** # reboot も # poweroff も反応しません。な、なんと # shutdown -r now に至っては「そげなコマンドあらへん」となります。 仕方がないので、今日はWindows上のGUIで再起動して寝ます・・・
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker公式Alpine Linuxイメージを使う

※編集途中です(2021-05-07_00:37) まえがき isoでインストールするといろいろ入ってますが、DockerHubから引っ張ってきたイメージはさらに絞られています。たとえば初期設定に便利なスクリプト"setup-alpine"とか"setup-ほにゃらら"系は全くありません。 (かと思えば、ifconfig や vi は入ってたりします) dockerを動かすだけ and/or イメージをDIYするならubuntuでも贅肉ありありなのでAlpine Linuxでマシンを組みました。 ・・・本当は512MBなVPSのリソースを無駄遣いできない、というビンボー事情です?・・・ ちなみに、"setup-ほにゃらら"はシェルスクリプトみたいなものらしいです。ソースはここにあります。 補足: gliderlabs/alpineというのもあり、イメージのサイズは400KBほど小さいです PS > docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE alpine latest(←3.13.5) 6dbb9cc54074 2 weeks ago 5.61MB gliderlabs/alpine latest 69bbd13383f7 2 years ago 5.19MB が、今日(2021-05-04)時点で2年前から更新されていません。 gliderlabs/alpine:latest # /bin/uname -a Linux *** 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 Linux かといってalpine:latest(2週間前に更新)のカーネルが最新かというと、そうではないようで alpine:latest # /bin/uname -a Linux *** 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 Linux と同じだったりして。ちなみに df の結果も同じなので、わけわかんないです? 原典とか 環境 最近のdockerな環境なら依存しないはず。 ちなみに現在は   Windows10 Education build 19042.928   PowerShell 5.1.19041.906   Docker Desktop for Windows version 20.10.5, build 55c4c88 特段、妙なアップデートはしてないはず。 操作 imageを引っ張ってきてコンテナを作る docker run PS> docker run -d -it -name ali alpine /bin/ash "ali" はコンテナ名です。ご自由につけてください。 /bin/ashは最初から入ってる唯一のshellです。/bin/shでも通じますが同じものが起動します。 docker exec 中に入ります。 PS> docker exec -it ali /bin/ash / # 初期設定 rootパスワードの設定 初期値は"空”ですので、早めに設定しましょう。 # passwd root Changing password for root New password:*** Retype password:*** passwd: password for root changed by root ホスト名の設定 デフォルトではイメージIDになってます。 # hostname aliwww "aliwww"がホスト名です。ご自由にどうぞ。 、、、といきたいところですが、「権限あらへん」エラーとなります。 現状、docker run する時に"--hostname aliwww"とするしか方法を知りません? ちなみに # echo "aliwww" > /tmp/t # hostname -F /tmp/t も同じエラーとなります。また、"setup-hostname"のソースにあるとおりに # echo "aliwww" > /etc/hostname すれば、エラーにはなりませんが再起動すると元のイメージIDに書き戻ります。 これは”df”の結果が: df # df Filesystem 1K-blocks Used Available Use% Mounted on ... /dev/sdc 263174212 2923424 246812632 1% /etc/resolv.conf /dev/sdc 263174212 2923424 246812632 1% /etc/hostname /dev/sdc 263174212 2923424 246812632 1% /etc/hosts ... となっているからだと思います(ということはDNSの設定もhostsの設定も一筋縄でいかない、ってことですね)。 タイムゾーンの設定 残念ながら https://wiki.alpinelinux.org/wiki/Setting_the_timezone 、、、と、apk前提ですので後述します。 keymapの設定 省略。JISキーボードな環境がないので検証できないからです。 isoインストールなら setup-keymap というコマンドで設定できますのでここのソースを読むといいかも、です。 基本的なパッケージのインストール apk使用開始前のおまじない なにはともあれ。 # apk update /etc/apk/repositoriesは最初から設定されてますから、よく紹介されている # cat > /etc/apk/repositories << EOF http://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/main http://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/community EOF は要らないです。 タイムゾーンの設定(再掲) 別にUTCでもいいけど。 # apk add tzdata # cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtim # cat "Asia/Tokyo" > /etc/timezone # apk del tzdata NTPの設定***執筆中*** # apk add openntpd # #rc-updateがないので一発でサービス登録できない。要調査。 SSHdのインストールと起動***執筆中*** # apk add openssh # #setup-sshdを参考にしようとしたがrc-updateがないのでサービスに登録できない。要調査。 必要に応じて /etc/ssh/sshd_config などを設定してください。 常用ユーザの設定 となると、rootでsshログインできないから、 # adduser boku Changing password for boku New password:*** Retype password:*** passwd: password for boku changed by root "boku"はユーザー名です。ご自由にどうぞ。 sudoのインストールと設定 rootになれないと困るから、 # apk add sudo # visudo ... boku ALL=(ALL) ALL を追記 ... そのほか IP固定****執筆中*** docker run する時に"--ip 192.168.0.2"とする方が便利かも。 # cat > /etc/network/interfaces << EOF auto lo iface lo inet loopback EOF #hostname同様、うまくいくとは思えない・・・ #ということはDNSやdefault gatewayも"docker run"時に指定するしかないのかなぁ 再起動***執筆中*** # /sbin/reboot # /sbin/poweroff いずれも反応しません。な、なんと # shutdown -r now に至っては「そげなコマンドあらへん」となります。 #仕方がないので、今日はWindows上のGUIで再起動して寝ます・・・
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker公式Alpine Linuxイメージの初期設定

まえがき isoでインストールするといろいろ入ってるらしいですが、DockerHubから引っ張ってきたイメージはさらに絞られています。たとえば初期設定に便利なスクリプト"setup-alpine"とか"setup-ほにゃらら"系は全くありません。 (かと思えば、ifconfig や vi は入ってたりします) dockerを動かすだけ and/or イメージをDIYするならubuntuでも贅肉ありありなのでAlpine Linuxでマシンを組みました。 ・・・本当は512MBなVPSのリソースを無駄遣いできない、というビンボー事情です?・・・ ちなみに、"setup-ほにゃらら"はシェルスクリプトみたいなものらしいです。ソースはここにあります。 補足: gliderlabs/alpineというのもあり、イメージのサイズは400KBほど小さいです PS > docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE alpine latest(←3.13.5) 6dbb9cc54074 2 weeks ago 5.61MB gliderlabs/alpine latest 69bbd13383f7 2 years ago 5.19MB が、今日(2021-05-04)時点で2年前から更新されていません。 gliderlabs/alpine:latest # /bin/uname -a Linux *** 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 Linux かといってalpine:latest(2週間前に更新)のカーネルが最新かというと、そうではないようで alpine:latest # /bin/uname -a Linux *** 5.4.72-microsoft-standard-WSL2 #1 SMP Wed Oct 28 23:40:43 UTC 2020 x86_64 Linux と同じだったりして。ちなみに df の結果も同じなので、わけわかんないです? 原典とか 環境 最近のdockerな環境なら依存しないはず。 ちなみに現在は   Windows10 Education build 19042.928   PowerShell 5.1.19041.906   Docker Desktop for Windows version 20.10.5, build 55c4c88 特段、妙なアップデートはしてないはず。 操作 imageを引っ張ってきてコンテナを作る docker run PS> docker run -d -it -name ali alpine /bin/ash "ali" はコンテナ名です。ご自由につけてください。 /bin/ashは最初から入ってる唯一のshellです。/bin/shでも通じますが同じものが起動します。 docker exec 中に入ります。 PS> docker exec -it ali /bin/ash / # 初期設定 rootパスワードの設定 初期値は"空”ですので、早めに設定しましょう。 # passwd root Changing password for root New password:*** Retype password:*** passwd: password for root changed by root ホスト名の設定 デフォルトではイメージIDになってます。 # hostname aliwww "aliwww"がホスト名です。ご自由にどうぞ。 、、、といきたいところですが、「権限あらへん」エラーとなります。 現状、docker run する時に"--hostname aliwww"とするしか方法を知りません? ちなみに # echo "aliwww" > /tmp/t # hostname -F /tmp/t も同じエラーとなります。また、"setup-hostname"のソースにあるとおりに # echo "aliwww" > /etc/hostname すれば、エラーにはなりませんが再起動すると元のイメージIDに書き戻ります。 これは”df”の結果が: # df Filesystem 1K-blocks Used Available Use% Mounted on ... /dev/sdc 263174212 2923424 246812632 1% /etc/resolv.conf /dev/sdc 263174212 2923424 246812632 1% /etc/hostname /dev/sdc 263174212 2923424 246812632 1% /etc/hosts ... となっているからだと思います(ということはDNSの設定もhostsの設定も一筋縄でいかない、ってことですね)。 タイムゾーンの設定 残念ながら https://wiki.alpinelinux.org/wiki/Setting_the_timezone 、、、と、apk前提ですので後述します。 keymapの設定 省略。JISキーボードな環境がないので検証できないからです。 isoインストールなら setup-keymap というコマンドで設定できますのでここのソースを読むといいかも、です。 基本的なパッケージのインストール apk使用開始前のおまじない なにはともあれ。 # apk update # apk upgrade /etc/apk/repositoriesは最初から設定されてますから、よく紹介されている # cat > /etc/apk/repositories << EOF http://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/main http://dl-cdn.alpinelinux.org/alpine/v$(cat /etc/alpine-release | cut -d'.' -f1,2)/community EOF は要らないです。 タイムゾーンの設定(再掲) 別にUTCでもいいけど。 # apk add tzdata # cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtim # cat "Asia/Tokyo" > /etc/timezone # apk del tzdata OpenRCのインストールと設定 # apk add openrc # sed -i 's/#rc_sys=""/rc_sys="lxc"/g' /etc/rc.conf # echo 'rc_provide="loopback net"' >> /etc/rc.conf # sed -i 's/^#\(rc_logger="YES"\)$/\1/' /etc/rc.conf # sed -i '/tty/d' /etc/inittab # sed -i 's/hostname $opts/# hostname $opts/g' /etc/init.d/hostname # sed -i 's/mount -t tmpfs/# mount -t tmpfs/g' /lib/rc/sh/init.sh # sed -i 's/cgroup_add_service /# cgroup_add_service /g' /lib/rc/sh/openrc-run.sh # rm -rf /var/cache/apk/ 原典はここです。 https://github.com/neeravkumar/dockerfiles/blob/master/alpine-openrc/Dockerfile これで rc-service でサービスを登録できるようになります。 NTPの設定 # apk add openntpd # vi /etc/ntpd.conf ... 近くのサーバを指すように修正 ... # rc-service openntpd start SSHdのインストールと起動 # apk add openssh # rc-service sshd start 必要に応じて /etc/ssh/sshd_config などを設定してください。 常用ユーザの設定 となると、rootでsshログインできないから、 # adduser boku Changing password for boku New password:*** Retype password:*** passwd: password for boku changed by root "boku"はユーザー名です。ご自由にどうぞ。 sudoのインストールと設定 rootになれないと困るから、 # apk add sudo # visudo ... boku ALL=(ALL) ALL を追記 ... そのほか IP固定***執筆中*** docker run する時に"--ip 192.168.0.2"とする方が便利かも。 # cat > /etc/network/interfaces << EOF auto lo iface lo inet loopback ... EOF ... #ということはDNSやdefault gatewayも"docker run"時に指定するしかないのかなぁ 再起動 # /sbin/reboot # /sbin/poweroff いずれも反応しません。な、なんと # shutdown -r now に至っては「そげなコマンドあらへん」となります。 WindowsなのでGUIで停止/再起動するさせてますが、結局はこれと同じかも。 PS> docker container stop ali vmtoolsみたいに正規の方法でシャットダウンしてるとは思えない=電源ブチ切りと同じかも? 次にすること VPS上にisoからインストールして、DOcker公式と比較する mini_httpdを入れてDocker勉強用のimage作り
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go言語】EchoとGORMでREST API(CRUD)を実装する

Go言語によるREST APIの実装方法を紹介します。 今回は例としてシンプルなCRUD API(GET、POST、PUT、DELETE)を実装します。 Go言語は1.16.3、フレームワークはEcho v4.2.2、O/RマッパはGORM v1.21.9、データベースはMySQL 8.0.21を利用します。 以下のような手順でREST APIを実装します。 Docker環境の構築 EchoでHTTPサーバを実装する GORMでデータベースにアクセスする 『Echo + GORM』でCRUD APIを実装する Docker環境の構築 Dockerを利用して検証環境を構築します。 実装の詳細は『ホットリロード x Go言語 x MySQL』なHTTPサーバのDocker環境構築手順で解説していますのであわせてご覧になってください。 Dockerfile FROM golang:1.16.3-buster WORKDIR /app COPY . /app RUN go mod tidy RUN go get github.com/cosmtrek/air CMD ["air"] docker-compose.yml version: '3' services: app: build: . ports: - '3030:3000' volumes: - .:/app depends_on: - db command: ["./start.sh"] db: image: mysql:8.0.21 ports: - '3306:3306' volumes: - go_mysql_data:/var/lib/mysql - ./docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d command: --default-authentication-plugin=mysql_native_password environment: MYSQL_USER: 'webuser' MYSQL_PASSWORD: 'webpass' MYSQL_ROOT_PASSWORD: 'pass' MYSQL_DATABASE: 'go_mysql8_development' volumes: go_mysql_data: start.sh #!/bin/bash -eu go mod tidy air MySQLのDockerイメージはコンテナ起動時に/docker-entrypoint-initdb.d配下のスクリプト(.sql、.sh、.sql.gz)を実行します。1 この機能を利用してサンプルデータを作成します。 docker-entrypoint-initdb.d/00_create_users.sql DROP TABLE IF EXISTS users; CREATE TABLE users ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(255), `email` VARCHAR(255), PRIMARY KEY (`id`) ); INSERT INTO users (id, name, email) VALUES (1, "Yamada", "yamada@example.com"); INSERT INTO users (id, name, email) VALUES (2, "Tanaka", "tanaka@example.com");% コンテナを起動する際にgo.modが必要になるため、go mod initでGo Modulesの初期化をします。 $ mkdir echo-gorm-crud-api-example cd $_ $ go mod init `basename $PWD` $ tree -L 2 . ├── Dockerfile ├── docker-compose.yml ├── docker-entrypoint-initdb.d │   └── 00_create.sql ├── go.mod └── start.sh EchoでHTTPサーバを実装する EchoとはGo言語の軽量フレームワークです。特にREST APIをGo言語で実装する際に人気です。 まずはEchoで実装したHTTPサーバへ正しくリクエストが届くのか確認をします。 Echoの公式ドキュメント#Guideを参考に実装します。 main.go package main import ( "net/http" "github.com/labstack/echo/v4" ) func hello(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") } func main() { e := echo.New() e.GET("/", hello) e.Logger.Fatal(e.Start(":3000")) // コンテナ側の開放ポートと一緒にすること } 動作確認 コンテナを起動してレスポンスが確認できればOKです。 ### 作業ディレクトリへ移動 $ cd /path/to/working_directory ### 現在のディレクトリの内容を確認 $ tree -L 2 . ├── Dockerfile ├── docker-compose.yml ├── docker-entrypoint-initdb.d │   └── 00_create_users.sql ├── go.mod ├── main.go └── start.sh ### バックグラウンドで起動 $ docker-compose up -d ### レスポンスが返ってくればOK $ curl localhost:3030 Hello, World! GORMでデータベースにアクセスする GORMとはGo言語のO/Rマッパです。Ruby on RailsのActive Recordに似た使い勝手のため、特にRuby経験者に人気です。 GORMのドキュメントを参考に、データベース接続の設定を追加します。 今回はデータベース接続に関するロジックを別パッケージで用意しました。 データベース接続の実装の詳細はGo言語でデータベース(MySQL)に接続する方法で紹介していますので併せてご覧になってください。 なお、GORMはV2.0とV1.0でデータベースの接続設定方法が異なるので注意が必要です。2 main.go package main import ( "echo-gorm-crud-api-example/database" "net/http" "github.com/labstack/echo/v4" ) func hello(c echo.Context) error { database.Connect() sqlDB, _ := database.DB.DB() defer sqlDB.Close() err := sqlDB.Ping() if err != nil { return c.String(http.StatusInternalServerError, "データベース接続失敗") } else { return c.String(http.StatusOK, "Hello, World!") } } func main() { e := echo.New() e.GET("/", hello) e.Logger.Fatal(e.Start(":3000")) } connect.go package database import ( "fmt" "os" "github.com/joho/godotenv" "gorm.io/driver/mysql" "gorm.io/gorm" ) // DBを使い回すことで、DBへのConnectとCloseを毎回しないようにする var DB *gorm.DB func Connect() { err := godotenv.Load() if err != nil { panic(err.Error()) } user := os.Getenv("DB_USER") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") database_name := os.Getenv("DB_DATABASE_NAME") dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4" DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { fmt.Println(err.Error()) } } 動作確認 データベースへ正常にアクセスできることを確認します。 ### 作業ディレクトリへ移動 $ cd /path/to/working_directory ### 現在のディレクトリの内容を確認 $ tree -L 2 . ├── Dockerfile ├── database │   └── connect.go ├── docker-compose.yml ├── docker-entrypoint-initdb.d │   └── 00_create_users.sql ├── go.mod ├── main.go └── start.sh ### バックグラウンドで起動 $ docker-compose up -d ### レスポンスが返ってくればOK $ curl localhost:3030 Hello, World! 『Echo + GORM』でCRUD APIを実装する ここからはEchoとGORMを利用してREST APIを実装する手順について紹介します。 今回はCRUD API(GET, PUT, POST, DELETE)を実装します。 GET API(全件取得)を実装する レコードの構造体(User)を作成し、JSON形式でレスポンスを返します。 JSONのプロパティ名は構造体のタグ(json: "xxx")で定義します。 main.go package main import ( "echo-gorm-crud-api-example/database" "net/http" "github.com/labstack/echo/v4" ) type User struct { Id int `json:"id"` Name string `json:"name"` Email string `json:"email"` } func getUsers(c echo.Context) error { users := []User{} database.DB.Find(&users) return c.JSON(http.StatusOK, users) } func main() { e := echo.New() database.Connect() sqlDB, _ := database.DB.DB() defer sqlDB.Close() e.GET("/users", getUsers) e.Logger.Fatal(e.Start(":3000")) } コンテナ起動後、curlでJSONレスポンスが取得できればOKです。 $ curl localhost:3030/users [{"id":1,"Name":"Yamada","email":"yamada@example.com"},{"id":2,"Name":"Tanaka","email":"tanaka@example.com"}] GET API(1件取得)を実装する Echoには、構造体にタグを付与することでリクエストデータとテーブルのレコードをバインドする機能があります。3 この機能によって、たとえば『/users/:idというエンドポイントに/users/2というリクエストがきた場合、ID = 2のレコードを取得する』といったことが可能になります。 具体的なソースコードは以下の通りです。 main.go package main import ( "echo-gorm-crud-api-example/database" "net/http" "github.com/labstack/echo/v4" ) type User struct { Id int `json:"id" param:"id"` Name string `json:"name"` Email string `json:"email"` } func getUsers(c echo.Context) error { users := []User{} database.DB.Find(&users) return c.JSON(http.StatusOK, users) } func getUser(c echo.Context) error { user := User{} if err := c.Bind(&user); err != nil { return err } database.DB.Take(&user) return c.JSON(http.StatusOK, user) } func main() { e := echo.New() database.Connect() sqlDB, _ := database.DB.DB() defer sqlDB.Close() e.GET("/users", getUsers) e.GET("/users/:id", getUser) e.Logger.Fatal(e.Start(":3000")) } コンテナ起動後、curlでJSONレスポンスが取得できればOKです。 $ curl localhost:3030/users/2 {"id":2,"name":"Tanaka","email":"tanaka@example.com"} 最終的なソースコード PUT・POST・DELETEを追加した最終的なソースコードは以下の通りです。 main.go package main import ( "go-docker-example/database" "net/http" "github.com/labstack/echo/v4" ) type User struct { Id int `json:"id" param:"id"` Name string `json:"name"` Email string `json:"email"` } func getUsers(c echo.Context) error { users := []User{} database.DB.Find(&users) return c.JSON(http.StatusOK, users) } func getUser(c echo.Context) error { user := User{} if err := c.Bind(&user); err != nil { return err } database.DB.Take(&user) return c.JSON(http.StatusOK, user) } func updateUser(c echo.Context) error { user := User{} if err := c.Bind(&user); err != nil { return err } database.DB.Save(&user) return c.JSON(http.StatusOK, user) } func createUser(c echo.Context) error { user := User{} if err := c.Bind(&user); err != nil { return err } database.DB.Create(&user) return c.JSON(http.StatusCreated, user) } func deleteUser(c echo.Context) error { id := c.Param("id") database.DB.Delete(&User{}, id) return c.NoContent(http.StatusNoContent) } func main() { e := echo.New() database.Connect() sqlDB, _ := database.DB.DB() defer sqlDB.Close() e.GET("/users", getUsers) e.GET("/users/:id", getUser) e.PUT("/users/:id", updateUser) e.POST("/users", createUser) e.DELETE("/users/:id", deleteUser) e.Logger.Fatal(e.Start(":3000")) } さいごに 最終的なソースコードはnishina555/echo-gorm-crud-api-exampleに公開しています。 認識違いや補足があればコメントいただけると助かります。 参考 EchoのBindでクエリとパスパラメータを構造体に入れる方法 GORM バージョン2の変更点 DockerHub『MySQL: Initializing a fresh instance』 ↩ GORM バージョン2の変更点 ↩ Echo: Binding Request Data ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Node.js+Express+MongoDB on Docker】環境構築 2021 (CTFのNoSQLi練習用サーバー)

これは? MongoDBに対するNoSQL Injectionを題材としたCTFの問題サーバーを用意したく作成したので実用には向いてません。 m1z0r3というCTFチームの勉強会用に作成したので所々m1z0r3とかmizoreとかあります。 https://qiita.com/sho_U/items/43f6483aac8ca45a12f6 の記事を参考に作らせていただきました。 用意するファイルたち 全体像 ├── .env ├── .gitignore ├── Dockerfile ├── challenge │   ├── controller │   │   └── initUserController.js │   ├── index.js │   ├── models │   │   └── User.js │   ├── package.json │   ├── routes │   │   └── index.js │   └── views │   ├── index.ejs │   └── js │   └── main.js ├── data │   └── db (空ディレクトリ) ├── docker-compose.yml ├── secret_file │   ├── db.env │   └── db_init │   └── mongo_init_user.js ├── setup.sh └── src (空ディレクトリ) 各ファイルの中身 .env MONGO_INITDB_ROOT_USERNAME=<mongoDBのrootのユーザー名> MONGO_INITDB_ROOT_PASSWORD=<mongoDBのrootのパスワード> MONGO_INITDB_DATABASE=<mongoDBのデータベース名> .gitignore node_modules/ data/ secret_file/ Dockerfile FROM node:12 WORKDIR /app RUN apt-get update && apt-get install -y vim RUN npm install docker-compose.yml version: '3' services: app: build: ./ container_name: nosqli-web ports: - "3004:3000" restart: always working_dir: /app tty: true volumes: - ./src:/app env_file: - ./secret_file/db.env command: bash networks: - mizore-network depends_on: - mongo mongo: image: mongo:latest container_name: nosqli-db ports: - "3005:27017" restart: always environment: MONGO_INITDB_ROOT_USERNAME: ${MONGO_INITDB_ROOT_USERNAME} MONGO_INITDB_ROOT_PASSWORD: ${MONGO_INITDB_ROOT_PASSWORD} MONGO_INITDB_DATABASE: ${MONGO_INITDB_DATABASE} volumes: - ./data/db:/data/db - ./secret_file/db_init/:/docker-entrypoint-initdb.d env_file: - ./secret_file/db.env command: - mongod networks: - mizore-network networks: mizore-network: (このネットワーク名は適当に変える) external: true secret_file/db.env DB_USER=<mongoDBのユーザー名(自分は.envと同じにした)> DB_PASS=<mongoDBのパスワード(自分は.envと同じにした)> DB_NAME=<mongoDBのデータベース名(自分は.envと同じにした)> secret_file/db_init/mongo_init_user.js let users = [ { user: "<mongoDBのユーザー名(これも自分は.envと同じにした)>", pwd: "<mongoDBのパスワード(これも自分は.envと同じにした)>", roles: [ { role: "dbOwner", db: "<mongoDBのデータベース名(これも自分は.envと同じにした)>" } ] } ]; for (let i = 0, length = users.length; i < length; ++i) { db.createUser(users[i]); } challenge/controller/initUserController.js const InitUser = require('../models/User'); const user = () => { let initUser = new InitUser({ username: "admin", password: "m1z0r3{...flag....}" }) initUser.save((error, data) => { if (error) { console.log(error); } console.log(data); }) let initUser2 = new InitUser({ username: "admin", password: "mmmmmmimmmmmmm_mm_mmmmmi" }) initUser2.save((error, data) => { if (error) { console.log(error); } console.log(data); }) let initUser3 = new InitUser({ username: "test", password: "passwd" }) initUser3.save((error, data) => { if (error) { console.log(error); } console.log(data); }) } module.exports = { user }; challenge/index.js const express = require("express"); const app = express(); const bodyParser = require("body-parser"); const routes = require("./routes"); const mongoose = require("mongoose"); mongoose.connect( `mongodb://${process.env.DB_USER}:${process.env.DB_PASS}@mongo:27017/<先程のmongoDBのデータベース名>`, { useNewUrlParser: true, useUnifiedTopology: true } ); // "@mongo" のmongoはdocker-compose.ymlの "mongo:" に対応しているのでlocalhostとかじゃできないので注意 // 後ポートの27017はコンテナ側のポート(":"で区切った時の右の方) // { useNewUrlParser: true, useUnifiedTopology: true } の部分はこれを丸コピ(他のだとうまく行かないという記事をみた) app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); app.set('view engine', "ejs"); app.use(routes); // 最初 /initUser にアクセスしてmongoDBにユーザー(データとしてのユーザー、mongoDBの認証関連のユーザーじゃない)のデータを入れる。 const initUserController = require("./controller/initUserController"); app.get("/initUser", initUserController.user); app.all("*", (req, res) => { return res.status(404).send({ message: '404 page not found' }); }); app.listen(3000, () => console.log("Listening on port 3004")); // 3000はコンテナの方のポートで、3004はホストで実際に開いてるポート challenge/package.json { "name": "mizore-app", "version": "0.0.0", "private": true, "scripts": { "start": "nodemon ./bin/www" }, "dependencies": { "bcrypt": "^5.0.0", "body-parser": "^1.19.0", "connect-flash": "^0.1.1", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "^3.1.5", "express": "~4.16.1", "express-ejs-layouts": "^2.5.0", "express-generator": "^4.16.1", "express-session": "^1.17.1", "express-validator": "^6.7.0", "http-errors": "~1.6.3", "http-status-codes": "^2.1.4", "method-override": "^3.0.0", "mongoose": "^5.11.9", "morgan": "~1.9.1", "nodemon": "^2.0.6", "passport": "^0.4.1", "passport-local-mongoose": "^6.0.1" } } challenge/models/User.js const mongoose = require("mongoose"); const Schema = mongoose.Schema; let User = new Schema({ username: { type: String }, password: { type: String } }, { collection: 'users' }); module.exports = mongoose.model("User", User); challenge/routes/index.js var express = require('express'); var router = express.Router(); var User = require("../models/User"); /* GET home page. */ router.get('/', function(req, res, next) { res.render('index', { title: 'Express' }); }); router.post("/login", (req, res) => { let { username, password } = req.body; if(username && password) { return User.find({ username, password }) .then((user) => { if(user.length == 1) { return res.json({logged: 1, message: `Login Successful, welcome back ${user[0].username} : ${user[0].password}` }); } else { return res.json({logged: 0, message: `Login Failed`}); } }) .catch(() => res.json({ message: "Something went wrong" })); } return res.json({ message: "Invalid username or password" }); }); module.exports = router; challenge/views/index.ejs <!DOCTYPE html> <html> <head> <title>NoSQLi Practice</title> <link rel='stylesheet' href='/stylesheets/style.css' /> </head> <body> <h1>NoSQLi Practice</h1> <p>Search User here</p> <form action="/login" method="post"> <label for="username">username:</label> <input type="text" id="username" name="username"><br/> <label for="password">password:</label> <input type="text" id="password" name="password"><br/> <input type="submit" value="login"> </form> </body> </html> challenge/views/js/main.js const login = document.getElementById("login"); const response = document.getElementById("response"); login.addEventListener("submit", e => { e.preventDefault(); fetch("/login", { method: "POST", body: new URLSearchParams(new FormData(e.target)) }) .then(resp => resp.json()) .then(data => { if(data.logged) { login.remove(); response.innerHTML = data.message; } else { response.innerHTML = data.message; } }); }); 構築する とりあえず以下のsetup.shを作る。 setup.sh # usage: ./setup.sh <containerID> # sudo rm -rf data/db/* && sudo rm -rf src/* # dc build # docker network create mizore-network # dc run app /bin/bash docker restart $1 && \ docker exec $1 npx express-generator -f --view=ejs && \ docker cp ./challenge/index.js $1:/app/ && echo "[OK] index.js" && \ docker cp ./challenge/package.json $1:/app/ && echo "[OK] package.json" && \ docker exec $1 mkdir /app/models && \ docker cp ./challenge/models/User.js $1:/app/models/ && echo "[OK] models/User.js" && \ docker cp ./challenge/routes/index.js $1:/app/routes/ && echo "[OK] routes/index.js" && \ docker cp ./challenge/views/index.ejs $1:/app/views/ && echo "[OK] views/index.ejs" && \ docker exec $1 mkdir /app/views/js && \ docker cp ./challenge/views/js/main.js $1:/app/views/js/ && echo "[OK] views/js/main.js" && \ docker exec $1 mkdir /app/controller && \ docker cp ./challenge/controller/initUserController.js $1:/app/controller/ && echo "[OK] controller/initUserController.js" && \ docker exec $1 npm install && \ docker-compose up -d && \ docker stop $1 && docker rm $1 && \ docker-compose exec app bash # docker-compose exec app node /app/index.js (コメントアウトしてるやつはコメントアウトされたままでいい) chmod +x setup.sh をした後、まずはdocker networkを以下のコマンドで作成する。 docker network create mizore-network # ネットワーク名はdocker-compose.ymlで定義したやつ 次に、以下のコマンドを実行する。 docker-compose build 最後らへんちょろっと数行赤いエラーが出るが気にしない。そのまま以下のコマンドを実行。 docker-compose run app /bin/bash これをするとコンテナが作成されてそのコンテナにbashで入ると思うので、exitで抜け出し、以下のコマンドを実行してそのコンテナIDをコピーする。 docker ps -a コンテナIDがコピーできたら、先程の setup.sh を以下のようにして実行する。 ./setup.sh <コピーしたコンテナID> うまく行けば、最後の docker-compose exec app bashでbashに入れる。 mongoDBにデータをセット + Webサーバー立ち上げ bashに入ったあと、/app ディレクトリにいると思うので、そのまま以下を実行。 node index.js そうするとエラーがなければ、console.log した Listening on 3004 みたいに表示されるはずなので、http://localhost:3004 にアクセスしてちゃんとサイトが表示されてるか確認する。 この段階ではまだmongoDBの中に何もデータが入っていない状態なので、どんなusername/passwordを入れても "Login Failed" になるはず。ちゃんとサイトが表示されたら、今度は http://localhost:3004/initUser にアクセスしてmongoDBにフラグがパスワードのadminとかのユーザーのデータを入れる(node index.jsしたコンソールにユーザーのデータが表示されたらちゃんとデータが入ったということ)。 その後、/initUserにまたアクセスしちゃうと重複してデータが追加されちゃうので、一回データが入ってることを確認したら、challenge/index.jsのapp.get("/initUser")みたいな所をコメントアウトする。 一通りちゃんと動きそうなら、docker-compose upの際に自動でnode index.jsをしてWebサーバーを起動してほしいので、docker-compose.ymlのcommand:のところを以下のように変更する。 docker-compose.yml app: # ... <略> ... command: bash # 上記を下記に変更!! app: # ... <略> ... command: node /app/index.js mongoDBの中の覗き方 mongoDBとうまく連携できなかったので詰まったので、実際にデータがちゃんと入ってるのか確認するため、mongoDBの中を確認する方法が下記。 まず、mongoの方のコンテナに以下のようにして入る。 docker-compose exec mongo bash これでmongoの方のコンテナのbashに入れるので、その後以下を実行する。 mongo <.envに書いたデータベース名> -u <.envに書いたユーザー名> -p これを実行すると "Enter Password" 聞かれるので、.envで書いたパスワードを入れる。 無事入れたら、以下のコマンド実行すれば代々できてるかどうかわかる。 > show collections # usersとか表示される > db.users.find() # これでフラグがパスワードのadminとか出てきたらちゃんと連携されてる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker コンテナのライフサイクル(メモ)

ざっくりとしたコンテナのライフサイクルは、 作成(create)→起動(start)→停止(stop)→破棄(rm)となります。 コンテナの作成・起動 docker run (オプション) イメージ (引数) イメージをダウンロードするdocker pull、コンテナを作成するdocker create、コンテナを起動するdocker start、これらの動きをまとめて実行してくれるのがdocker run。 docker runコマンドはコンテナの作成・起動、イメージがない場合はイメージのダウンロードも行ってくれます。 コンテナの停止 docker stop コンテナ名 コンテナの削除 docker rm コンテナ名
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Streamlit Docker on Heroku

TL;DR 公式のデプロイドキュメントに色々載っているのでその通りにやりましょう やったこと streamlitのお触り streamlit on docker Herokuへのデプロイ Streamlitとは pythonだけで簡単にアプリケーションを作れるフレームワーク。フロントエンドの知識なくてもオッケー。 インタラクティブな機能はもちろん、matplotlib、plotly、bokehなど一通り可視化ツールのサポートをしてるので、可視化結果を見せるのが非常に楽。 ※Dashとかも似たようなものらしいですが、Streamlitはより単純に、素早く書けるのがメリットのようです。その分柔軟性は低い?とか(比較検証はしてません) できたもの NBAのスタッツ(2018~19)とサラリー(2019~2020)を可視化。 こんな感じで見れる。 アプリはここにあります https://demo-lit-app.herokuapp.com/ コードはこっち https://github.com/iusami/demo-streamlit-heroku 未解決の話 cache機能があるのでそれでplot部分を高速化できないかなと思ったが、エラーが出て上手く動作しなかった。 ページの遷移を行うために_SessionStateを使ってるので単純にはキャッシュできないっぽい。 データをコンテナに込めてしまっているが、ほんとはどこかからダウンロードするようにしたい。(データサーバを用意するのがめんどくさかった。) 今後やってみたいこと 機械学習使ったインタラクティブなアプリを作れそう。パラメータ変更、学習、推論までおそらく全部実装できる。 参考文献 https://docs.streamlit.io/en/stable/index.html https://discuss.streamlit.io/t/streamlit-deployment-guide-wiki/5099 https://github.com/ash2shukla/streamlit-heroku
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go-gin + mysql + gorm + Dockerで開発用APIサーバーを立ててみた

簡易なウェブアプリが作りたくて、そのときのメモです。 go-ginでAPIサーバーを立ててデータベースと通信できるようにします。 Github Repositoryを作りました。 OS macOS Catalina 使う技術スタック gin (ウェブサーバー) dlv (デバッグ) air (ホットリロード) gorm (ORMライブラリ) mysql golang-migrate (マイグレーション) docker あると便利なアプリ postman (rest api通信確認) goland (デバッグ、開発) sequel pro (データベース確認) ディレクトリ構造 最終的には以下のようになっています。 . ├── backend │   ├── Dockerfile.local │   ├── article │   │   └── article.go │   ├── go.mod │   ├── go.sum │   ├── handler │   │   ├── articleFunc.go │   │   └── userFunc.go │   ├── lib │   │   └── sql_handler.go │   ├── main.go │   ├── migration │   │   ├── main.go │   │   └── migrations │   │   ├── 1_articles.down.sql │   │   ├── 1_articles.up.sql │   │   ├── 2_users.down.sql │   │   └── 2_users.up.sql │   ├── tmp │   │   └── main │   └── user │   └── user.go ├── docker-compose.yml └── mysql ├── Dockerfile └── my.cnf 9 directories, 18 files 気をつけるところ github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダのところは各自書き換えてください。 ディレクトリ next-gin-mysqlという名前で立ち上げます。(nextをフロントエンドとして立ち上げたいのですが、記事の長さ的にそれは次回にします) $ mkdir next-gin-mysql バックエンド側 まずディレクトリを作ります。 next-gin-mysql $ mkdir backend go moduleの用意 go modulesの用意をします。 github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダで初期化するとやりやすいと思います。 next-gin-mysql/backend $ go mod init github.com/greenteabiscuit/next-gin-mysql/backend next-gin-mysql/backend $ go get -u github.com/gin-gonic/gin これで ginとその依存パッケージがダウンロードされます。 それぞれのファイルの用意 サーバーを立ち上げるmain.goです。データベース用のlib、記事とユーザーのモデル、そしてハンドラはあとで書きます。 main.go package main import ( "os" "time" "github.com/gin-gonic/gin" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/greenteabiscuit/next-gin-mysql/backend/handler" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/joho/godotenv" "github.com/gin-contrib/cors" ) func main() { if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { panic(err) } } article := article.New() user := user.New() lib.DBOpen() defer lib.DBClose() r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{ "http://localhost:3000", }, AllowMethods: []string{ "POST", "GET", "OPTIONS", }, AllowHeaders: []string{ "Access-Control-Allow-Credentials", "Access-Control-Allow-Headers", "Content-Type", "Content-Length", "Accept-Encoding", "Authorization", }, AllowCredentials: true, MaxAge: 24 * time.Hour, })) r.GET("/article", handler.ArticlesGet(article)) r.POST("/article", handler.ArticlePost(article)) r.POST("/user/login", handler.UserPost(user)) r.Run(os.Getenv("HTTP_HOST") + ":" + os.Getenv("HTTP_PORT")) // listen and serve on 0.0.0.0:8080 } 記事を扱うarticleのハンドラパッケージです。 handler/articleFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/gin-gonic/gin" ) func ArticlesGet(articles *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { result := articles.GetAll() c.JSON(http.StatusOK, result) } } type ArticlePostRequest struct { Title string `json:"title"` Description string `json:"description"` } func ArticlePost(post *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { requestBody := ArticlePostRequest{} c.Bind(&requestBody) item := article.Article{ Title: requestBody.Title, Description: requestBody.Description, } post.Add(item) c.Status(http.StatusNoContent) } } userのハンドラ関数は以下です。 handler/userFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/gin-gonic/gin" ) func UsersGet(users *user.Users) gin.HandlerFunc { return func(c *gin.Context) { result := users.GetAll() c.JSON(http.StatusOK, result) } } type UserPostRequest struct { Username string `json:"username"` Password string `json:"password"` } func UserPost(post *user.Users) gin.HandlerFunc { return func(c *gin.Context) { requestBody := UserPostRequest{} c.Bind(&requestBody) item := user.User{ Username: requestBody.Username, Password: requestBody.Password, } post.Add(item) c.Status(http.StatusNoContent) } } articleの構造体などを定義するファイルです。 article/article.go package article import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type Article struct { Title string `json:"title"` Description string `json:"description"` } type Articles struct { Items []Article } func New() *Articles { return &Articles{} } func (r *Articles) Add(a Article) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Articles) GetAll() []Article { db := lib.GetDBConn().DB var articles []Article if err := db.Find(&articles).Error; err != nil { return nil } return articles } userの構造体をまとめたファイルです。ちょっとここはテキトーなのでもう少し直したい、、、 user/user.go package user import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type User struct { Username string `json:"username"` Password string `json:"password"` } type Users struct { Items []User } func New() *Users { return &Users{} } func (r *Users) Add(a User) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Users) GetAll() []User { db := lib.GetDBConn().DB var users []User if err := db.Find(&users).Error; err != nil { return nil } return users } mysqlと接続を行う sql_handler.goも追加します。 lib/sql_handler.go package lib import ( "fmt" "os" "time" "gorm.io/gorm" "gorm.io/driver/mysql" ) // SQLHandler ... type SQLHandler struct { DB *gorm.DB Err error } var dbConn *SQLHandler // DBOpen は DB connectionを張る。 func DBOpen() { dbConn = NewSQLHandler() } // DBClose は DB connectionを張る。 func DBClose() { sqlDB, _ := dbConn.DB.DB() sqlDB.Close() } // NewSQLHandler ... func NewSQLHandler() *SQLHandler { user := os.Getenv("DB_USERNAME") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") fmt.Println(user, password, host, port) var db *gorm.DB var err error // Todo: USE_HEROKU = 1のときと場合分け if os.Getenv("USE_HEROKU") != "1" { dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + dbName + "?parseTime=true&loc=Asia%2FTokyo" db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { panic(err) } } /*else { var ( instanceConnectionName = os.Getenv("DB_CONNECTION_NAME") // e.g. 'project:region:instance' ) dbURI := fmt.Sprintf("%s:%s@unix(/cloudsql/%s)/%s?parseTime=true", user, password, instanceConnectionName, database) // dbPool is the pool of database connections. db, err = gorm.Open(mysql.Open(dbURI), &gorm.Config{}) if err != nil { panic(err) } }*/ sqlDB, _ := db.DB() //コネクションプールの最大接続数を設定。 sqlDB.SetMaxIdleConns(100) //接続の最大数を設定。 nに0以下の値を設定で、接続数は無制限。 sqlDB.SetMaxOpenConns(100) //接続の再利用が可能な時間を設定。dに0以下の値を設定で、ずっと再利用可能。 sqlDB.SetConnMaxLifetime(100 * time.Second) sqlHandler := new(SQLHandler) db.Logger.LogMode(4) sqlHandler.DB = db return sqlHandler } // GetDBConn ... func GetDBConn() *SQLHandler { return dbConn } // BeginTransaction ... func BeginTransaction() *gorm.DB { dbConn.DB = dbConn.DB.Begin() return dbConn.DB } // Rollback ... func RollBack() { dbConn.DB.Rollback() } ローカル開発環境の整備 docker-composeで一発でサーバーをすべて立ち上げるようにします。このときについでに mysqlの準備とデバッグサーバーの準備もしておきます。 docker-compose.yml version: '3' services: go: build: context: ./backend dockerfile: Dockerfile.local volumes: - ./backend:/go/src/backend working_dir: /go/src/backend environment: TZ: Asia/Tokyo ports: - 8080:8080 - 2345:2345 security_opt: - apparmor:unconfined cap_add: - SYS_PTRACE mysql: build: ./mysql environment: TZ: Asia/Tokyo MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: backend ports: - 13306:3306 volumes: - mysql_volume:/var/lib/mysql volumes: mysql_volume: go用のDockerfileです。今後デプロイ用のDockerfileと分けるかもしれないので、一応Dockerfile.localにしておきます。 backend/Dockerfile.local FROM golang:1.15.2 COPY . /go/src/sample WORKDIR /go/src/sample RUN go get -u github.com/cosmtrek/air RUN go get -u github.com/go-delve/delve/cmd/dlv CMD ["air", "-c", ".air.toml"]** ホットリロードするために、airを使います。 airの設定ファイルである.air.tomlはairの本家レポジトリから引っ張ってくることができます。 backend/.air.toml # Config file for [Air](https://github.com/cosmtrek/air) in TOML format # Working directory # . or absolute path, please note that the directories following must be under root. root = "." tmp_dir = "tmp" [build] # Just plain old shell command. You could use `make` as well. cmd = "go build -o ./tmp/main ." # Binary file yields from `cmd`. bin = "tmp/main" # Customize binary. full_bin = "APP_ENV=dev APP_USER=air /go/bin/dlv exec ./tmp/main --headless=true --listen=:2345 --api-version=2 --accept-multiclient" # Watch these filename extensions. include_ext = ["go", "tpl", "tmpl", "html"] # Ignore these filename extensions or directories. exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "storage"] # Watch these directories if you specified. include_dir = [] # Exclude files. exclude_file = [] # Exclude unchanged files. exclude_unchanged = true # This log file places in your tmp_dir. log = "air.log" # It's not necessary to trigger build each time file changes if it's too frequent. delay = 1000 # ms # Stop running old binary when build errors occur. stop_on_error = true # Send Interrupt signal before killing process (windows does not support this feature) send_interrupt = false # Delay after sending Interrupt signal kill_delay = 500 # ms [log] # Show log time time = false [color] # Customize each part's color. If no color found, use the raw app log. main = "magenta" watcher = "cyan" build = "yellow" runner = "green" [misc] # Delete tmp directory on exit clean_on_exit = true mysqlの設定ファイルです。 mysql/my.cnf # MySQLサーバーへの設定 [mysqld] # 文字コード/照合順序の設定 character_set_server=utf8mb4 collation_server=utf8mb4_bin # タイムゾーンの設定 default_time_zone=SYSTEM log_timestamps=SYSTEM # デフォルト認証プラグインの設定 default_authentication_plugin=mysql_native_password # mysqlオプションの設定 [mysql] # 文字コードの設定 default_character_set=utf8mb4 # mysqlクライアントツールの設定 [client] # 文字コードの設定 default_character_set=utf8mb4 mysqlのDockerfileです。 FROM mysql:8.0.21 # FROM mysql@sha256:77b7e09c906615c1bb59b2e9d7703f728b1186a5a70e547ce2f1079ef4c1c5ca RUN echo "USE mysql;" > /docker-entrypoint-initdb.d/timezones.sql && mysql_tzinfo_to_sql /usr/share/zoneinfo >> /docker-entrypoint-initdb.d/timezones.sql COPY ./my.cnf /etc/mysql/conf.d/my.cnf 環境変数も設定しておきます。本番環境では見せないようにしましょう。 DB_HOSTはコンテナの名前で選ぶので、mysqlにしてあります。 backend/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend 立ち上げてみる サーバーを立ち上げてみましょう。以下のようにエラーが起きずに 2345が出てきたらとりあえずOKです。 docker-compose up --build mysql_1 | 2021-05-04T16:29:33.735276+09:00 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. mysql_1 | 2021-05-04T16:29:33.770064+09:00 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.21' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL. go_1 | watching user go_1 | building... go_1 | running... go_1 | API server listening at: [::]:2345 golandでの設定 golandで開発するのがやりやすいと思います。 今はデバッグのサーバしか立ち上がっていないので、ここで設定を行います。 delveのプロセスを走らせることで、APIサーバーも立ち上がるようになります。 メニューのdebugをクリックし、go remoteを選びます。 適当な名前をつけて、applyとdebugをクリックします。 これでコマンドラインでもサーバが立ち上がっています。ただまだマイグレーションができていないので、データベースなどからデータが取得できません。 go_1 | API server listening at: [::]:2345 go_1 | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. go_1 | go_1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. go_1 | - using env: export GIN_MODE=release go_1 | - using code: gin.SetMode(gin.ReleaseMode) go_1 | go_1 | [GIN-debug] GET /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlesGet.func1 (3 handlers) go_1 | [GIN-debug] POST /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlePost.func1 (3 handlers) go_1 | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default go_1 | [GIN-debug] Listening and serving HTTP on :8080 マイグレーション golang-migrateを使ってmysqlデータベースを立ち上げます。migrationというフォルダをbackend直下に作ります。 backend $ mkdir migration 以下のような構造になります。main.goでmigrationの処理を書きます。 migration $ tree . ├── main.go └── migrations ├── 1_articles.down.sql ├── 1_articles.up.sql ├── 2_users.down.sql └── 2_users.up.sql migrationの.env 先ほどと同じになりますが、なるべく見せないようにしましょう。今回はローカルなので大丈夫かと思いますが。 migration/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend migration の main.go migration/main.go package main import ( "database/sql" "flag" "fmt" "os" "time" _ "github.com/go-sql-driver/mysql" "github.com/golang-migrate/migrate" "github.com/golang-migrate/migrate/database/mysql" _ "github.com/golang-migrate/migrate/source/file" "github.com/joho/godotenv" "github.com/pkg/errors" ) var migrationFilePath = "file://./migrations/" func main() { fmt.Println("start migration") flag.Parse() command := flag.Arg(0) migrationFileName := flag.Arg(1) if command == "" { showUsage() os.Exit(1) } if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { fmt.Println(errors.Wrap(err, "load error .env")) } } m := newMigrate() version, dirty, _ := m.Version() force := flag.Bool("f", false, "force execute fixed sql") if dirty && *force { fmt.Println("force=true: force execute current version sql") m.Force(int(version)) } switch command { case "new": newMigration(migrationFileName) case "up": up(m) case "down": down(m) case "drop": drop(m) case "version": showVersionInfo(m.Version()) default: fmt.Println("\nerror: invalid command '", command, "'") showUsage() os.Exit(0) } } func generateDsn() string { apiRevision := os.Getenv("API_REVISION") var dsn string if apiRevision == "release" { dsn = os.Getenv("DATABASE_URL") + "&multiStatements=true" // heroku対応 } else { user := os.Getenv("DB_USERNAME") pass := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&multiStatements=true", user, pass, host, port, dbName) } return dsn } func newMigrate() *migrate.Migrate { dsn := generateDsn() db, openErr := sql.Open("mysql", dsn) if openErr != nil { fmt.Println(errors.Wrap(openErr, "error occurred. sql.Open()")) os.Exit(1) } driver, instanceErr := mysql.WithInstance(db, &mysql.Config{}) if instanceErr != nil { fmt.Println(errors.Wrap(instanceErr, "error occurred. mysql.WithInstance()")) os.Exit(1) } m, err := migrate.NewWithDatabaseInstance( migrationFilePath, "mysql", driver, ) if err != nil { fmt.Println(errors.Wrap(err, "error occurred. migrate.NewWithDatabaseInstance()")) os.Exit(1) } return m } func showUsage() { fmt.Println(` ------------------------------------- Usage: go run migration/main.go <command> Commands: new FILENAME Create new up & down migration files up Apply up migrations down Apply down migrations drop Drop everything version Check current migrate version -------------------------------------`) } func newMigration(name string) { if name == "" { fmt.Println("\nerror: migration file name must be supplied as an argument") os.Exit(1) } base := fmt.Sprintf("./migration/migrations/%s_%s", time.Now().Format("20060102030405"), name) ext := ".sql" createFile(base + ".up" + ext) createFile(base + ".down" + ext) } func createFile(fname string) { if _, err := os.Create(fname); err != nil { panic(err) } } func up(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Up() if err != nil { if err.Error() != "no change" { panic(err) } fmt.Println("\nno change") } else { fmt.Println("\nUpdated:") version, dirty, err := m.Version() showVersionInfo(version, dirty, err) } } func down(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Steps(-1) if err != nil { panic(err) } else { fmt.Println("\nUpdated:") showVersionInfo(m.Version()) } } func drop(m *migrate.Migrate) { err := m.Drop() if err != nil { panic(err) } else { fmt.Println("Dropped all migrations") return } } func showVersionInfo(version uint, dirty bool, err error) { fmt.Println("-------------------") fmt.Println("version : ", version) fmt.Println("dirty : ", dirty) fmt.Println("error : ", err) fmt.Println("-------------------") } マイグレーションのためのsqlファイル 1_articles.down.sql drop table if exists articles 1_articles.up.sql create table if not exists articles ( id integer auto_increment primary key, title varchar(40), description varchar(40) ) 2_users.down.sql drop table if exists users 2_users.up.sql create table if not exists users ( id integer auto_increment primary key, username varchar(40), password varchar(40) ) マイグレーションする dockerコンテナに入ってマイグレーションします。go run main.go upでマイグレーションが行われます。 $ docker exec -it container_name bash # cd migration # go run main.go up start migration Before: ------------------- version : 0 dirty : false error : no migration ------------------- Updated: ------------------- version : 2 dirty : false error : <nil> ------------------- これでdbでもテーブルができているのを確認したらOKです! Sequel Proなどのアプリで確認できます。 PostmanでのAPI確認 これは Postmanというデスクトップアプリで確認することができます。 Post JSON形式で送ります。 GET データベースにも hoge と hello worldが入っているのが確認できました。 コマンドラインでもカラフルに表示されています。 まとめ air + gin + gorm + golang-migrate + mysql + delve + dockerで開発用APIサーバーを立ち上げることができました。 今後試してみたいこと herokuにデプロイしてみる userのところの処理がテキトーなままなので直します、、、 フロントエンド(next + reactなど)とつなげてみる(そもそもレポジトリ名にnextを含んでいたので繋げようと思ったのですが、記事が長すぎてしまったので次回以降やりたいと思います。) 参考 go-ginでサクッとRESTAPIを構築する golang,docker,mysqlの環境をherokuにデプロイする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『ホットリロード x Go言語 x MySQL』なHTTPサーバのDocker環境構築手順

データベース接続をするGo言語製HTTPサーバのDocker環境構築について紹介します。 『コンテナ上のHTTPサーバとデータベースの疎通確認をする』をゴールとします。 Goは1.16.3、データベースはMySQL 8.0.21を利用します。 ホットリーロド可能なHTTPサーバのDocker環境を構築 GO言語で実装されたHTTPサーバをDocker化します。 構築手順の詳細は【Go言語】ホットリロード可能なHTTPサーバのDocker環境構築手順で解説をしているので、あわせてご覧になってください。 ホットリロードはAirで行います。Airの詳細についてはGo言語のホットリロードツール『Air』でコードの修正を即時反映させるをご覧になってください。 main.go package main import ( "log" "net/http" ) func rootHander(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Header().Set("Content-Type", "text/html; charset=utf8") w.Write([]byte("こんにちは")) } func main() { http.HandleFunc("/", rootHander) log.Fatal(http.ListenAndServe(":3000", nil)) } Dockerfile FROM golang:1.16.3-buster # コンテナの作業ディレクトリにローカルのファイルをコピー WORKDIR /app COPY . /app # 必要なパッケージをインストール RUN go mod tidy # Airをインストール RUN go install github.com/cosmtrek/air@v1.27.3 # airコマンドでGoファイルを起動 CMD ["air"] docker-compose.yml version: '3' services: app: build: . ports: - '3030:3000' # ローカルの3030番ポートでコンテナの3000番ポートに接続 volumes: - .:/app # ローカルとコンテナのディレクトリをバインドマウント(同期) - go_path:/go # パッケージやバイナリファイルのインストール先($GOPATH)を永続化 command: ["./start.sh"] volumes: go_path: start.sh #!/bin/bash -eu go mod tidy air 動作確認 コンテナ上のHTTPサーバにリクエストが届くことを確認します。 curl localhost:3030でレスポンスが返ってくればOKです。 ### 作業ディレクトリの作成と移動 $ mkdir go-docker-example && cd $_ ### go.modの作成 $ go mod init `basename $PWD` ### ファイルの確認 $ ls Dockerfile docker-compose.yml go.mod main.go start.sh ### バックグラウンドで起動 $ docker-compose up -d ### 接続の確認 $ curl localhost:3030 こんにちは データベース接続設定を追加 Go言語でデータベース(MySQL)に接続する方法で紹介した手順をもとにmain.goの修正と、データベース接続に関するパッケージの作成をします。 main.go package main import ( "go-docker-example/database" "log" "net/http" ) func rootHander(w http.ResponseWriter, r *http.Request) { db := database.Connect() defer db.Close() err := db.Ping() if err != nil { w.Write([]byte("データベース接続失敗")) return } else { w.WriteHeader(200) w.Header().Set("Content-Type", "text/html; charset=utf8") w.Write([]byte("データベース接続成功")) } } func main() { http.HandleFunc("/", rootHander) log.Fatal(http.ListenAndServe(":3000", nil)) } database/connect.go package database import ( "database/sql" "fmt" "os" "github.com/joho/godotenv" _ "github.com/go-sql-driver/mysql" ) func Connect() *sql.DB { err := godotenv.Load() if err != nil { fmt.Println(err.Error()) } user := os.Getenv("DB_USER") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") database_name := os.Getenv("DB_DATABASE_NAME") dbconf := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4" db, err := sql.Open("mysql", dbconf) if err != nil { fmt.Println(err.Error()) } return db } .env DB_USER=webuser DB_PASSWORD=webpass DB_HOST=db DB_PORT=3306 DB_DATABASE_NAME=go_mysql8_development 次にdocker-compose.ymlの修正をします。 データベースコンテナを新しく追加し、HTTPサーバとデータベースがDocker上で疎通できるようにします。 docker-compose.yml version: '3' services: app: build: . ports: - '3030:3000' volumes: - .:/app - go_path:/go db: image: mysql:8.0.21 ports: - '3306:3306' command: --default-authentication-plugin=mysql_native_password environment: MYSQL_USER: 'webuser' MYSQL_PASSWORD: 'webpass' MYSQL_ROOT_PASSWORD: 'pass' MYSQL_DATABASE: 'go_mysql8_development' volumes: go_path: 動作確認 コンテナを起動し、HTTPサーバとデータベースの疎通確認を行います。 『データベース接続成功』のレスポンスが返ってくればOKです。 ### 作業ディレクトリへ移動 $ cd /path/to/go-docker-example ### ファイルの確認 $ tree -L 1 ├── Dockerfile ├── database │   └── connect.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── main.go └── start.sh ### バックグラウンドで実行 $ docker-copmose up -d ### 接続の確認 $ curl localhost:3030 データベース接続成功 さいごに 認識違いや補足があればコメントいただけると助かります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerメモ/画像補間

日記: Docker/DockerHub関連 環境 OS: ubuntu 20.04 intel 64bit / Windows 10 Hyper-V - apt install -y git -…
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerメモ/画像補間/samba

日記: Docker/DockerHub関連 環境 OS: ubuntu 20.04 intel 64bit / Windows 10 Hyper-V - sudo snap install do…
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Denoの開発環境をDocker+VSCodeで構築

Denoを使うと、TypeScriptをコンパイルせずに実行できます。これをDockerコンテナ上で実行し、DenoもインストールすることなくTypeScriptを動かしてみましょう。 環境 Debian version 10.9 (ChromeOS 89.0 Chrostini Linux) Docker 20.10.6 deno 1.9.2 v8 9.1.269.5 typescript 4.2.2 Visual Studio Code 1.55.2 構築 Dockerfile,docker-compose.ymlを配置してコンテナを立ち上げVSCode Remote Containerでアタッチするだけです。 Dockerfile FROM debian:stable-slim WORKDIR /app RUN apt-get update && apt-get install -y \ curl \ zip \ unzip \ git \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* RUN curl -fsSL https://deno.land/x/install/install.sh | sh ENV DENO_INSTALL="/root/.deno" ENV PATH="$DENO_INSTALL/bin:$PATH" docker-compose.yml version: '3' services: app: build: . volumes: - .:/app $ echo 'console.log("Hello, world")' > main.tsのように適当なTypeScriptファイルを作って、$ docker-compose buildして$ docker-compose run --rm app deno run main.tsして動作したら成功です。 VS Codeの設定 vscode-deno拡張機能をインストールして下記設定で有効にする必要があります。VS CodeはデフォルトでTypeScriptに対応しているのでそれなりに動いてくれるのですが、エディタ上でnoImplictAnyが効かなかったりしました。 .vscode/settings.json { "deno.enable": true } 参考文献 【VSCode・Deno】拡張機能:Remote - Containers で Deno の開発環境を構築 - 開発覚書はてな版
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

独学・未経験者がポートフォリオを作った【Rails / Vue.js / Docker / AWS】

はじめに はじめまして! 未経験者が独学でポートフォリオを作成しましたので、紹介させてください。 About Me 私は33歳で現在、看護師として病院に勤務しています。 看護師として働く前は、テレコミュニケーターとして某プロバイダーの派遣社員として働いていました。 そういった経歴でパソコンを触るのが好きだったこともあり、独自で作成した動画を使って、部署内で勉強会を開催するなどしていました。 昨今の新型コロナウイルスの影響で、従来の「集まって教育・学習・研修をする」ということができなくなり、IT機器やオンラインの重要性を個人的にも感じていました。 「もっと看護業界、医療業界が加速するようなことがしたい」と考えるようになり、以前から興味のあった「Webエンジニア」への転職を目指しました。 2020年5月からプログラミング学習を開始しました。 紹介させていただく、ポートフォリオの作成期間は約3ヶ月半(2021年1月〜4月はじめ) ポートフォリオ紹介 アプリ名「With Nurse」 URL:https://www.withnurse.net/ Githubリポジトリ TOPページ レスポンシブ対応 ログイン後TOPページ 概要 看護師専用の記事投稿+学習サービスです。 記事投稿機能やVue.jsを使った一部SPAのゲーム機能を実装しています。 なぜ開発したか About Meでもお話したように、私は看護師として働きながら、プログラミング学習をしています。 その中で感じた、以下の理由で開発しようと考えました。 1.同業者との情報共有でスッと理解できる 現場では先輩から看護について教育を受けます。その中でテキストや文献を読むだけではなかなか理解できなかったことを、その先輩の説明でスッと理解できる場面がたくさんあります。 それは先輩だけでなく、同期や後輩の言葉が理解を深めるということも多々あります。 もちろん二次情報になるので、勘違いや見当違いなこともあるわけなので、盲信できるわけではありませんが、現場で起きている「スッと理解できる」という場面をネット上でもできないかと考え、このアプリを開発しました。 2.Qiitaに感動した プログラミング学習を初めて、一番驚いたのがたくさんの人が情報発信をしていたということです。 ポートフォリオを作成しながら、わからない・できないところはググって実装していきました。 ほとんどのことをネット上で調べることができました。 たくさんの情報発信をエンジニアの方たちがされていること、そして同じエンジニアの人たちと知識を共有しているエンジニア業界の仕組みに感動したからです。 公式ドキュメントなどで一次情報を調べることは必須ですが、理解しづらいことが、ある人の情報発信で「スッと理解できる」Qiitaのようなサービスがあればいいなと考えたからです。 3.学習サイト、スキルトレーニングサイトの重要性 私は物事を学習する上でアウトプットに勝るものはないと考えています。 私自身も勉強会に参加するときよりも、開催するほうが自分の知識やスキルが向上することを実感していたからです。 自分の得意な分野や学習を始めた分野のアウトプットをする場所がインターネット上にあればとても有用だと思い、記事投稿サービスを作りました。 また看護師として10年勤務して、「このスキルを鍛えておくと、非常に有用である」と思った内容をゲームとして提供しています。 タイピングゲームを実装しているのですが、それもその一つです。 使用技術 HTML/CSS Bootstrap SASS javascript jQuery Vue.js Ruby 2.6.3 Ruby on Rails 6.0.3 Rubocop (コード解析ツール) RSpec (テスト) Docker/Docker-compose(ローカル開発環境からデプロイまで) AWS(VPC, EC2, Route53, ELB, S3, RDS, ACM) 実装機能 基本機能 新規会員登録・ログイン機能 Googleアカウントログイン機能 パスワードリセット機能 記事一覧機能(ページネーション) 記事ソート機能(タグで絞り込み) 記事検索機能(テキスト検索) タグ機能 記事詳細画面閲覧機能 タイピングゲーム機能(データー登録はしない) いいね数表示機能 お問い合わせ機能 ログイン後機能 ユーザーマイページ表示機能( フォロー・フォロワー表示・My記事一覧表示・My記事投稿数表示機能 ) ユーザー情報変更機能(アイコン画像・ユーザーネーム・メールアドレス・パスワード) MYユーザー削除機能 タイピングゲーム機能(スコア、プレイ回数表示、保存) 記事いいね機能(非同期通信) ユーザーフォロー機能 記事投稿機能(ActionText) 記事編集機能 画像投稿機能(ActiveStorage経由でS3バケットに保存) 管理者機能 ユーザー一覧表示 ユーザー削除機能 インフラ構成図 ER図 ポートフォリオの工夫したところ ユーザー対象は看護師で、業務上よく使う、officeのWordのUIに比較的近い「Action Text」を採用 課題 技術選定、DB設計など、初期の段階で構想がめちゃくちゃ甘かった 未経験からのWEB系エンジニアへの転職が当面の目標であったため 多く採用されている 学習がしやすい(公式ドキュメント、実装事例が多い、ユーザーが多いため、質問しやすい) 上記の理由からRuby on Rails、Vueを使うとは決めていました。 しかしAWSやDockerの採用などはなぜこの技術なのかという技術的判断よりも、デファクトスタンダードであろう技術であり、ポートフォリオ作成時点でキャッチできた情報で手探りで作りました。 なぜその技術が必要なのかという判断をするための知識もまだまだ不足しているため、ポートフォリオの要件に合わせて、ブラッシュアップしていく必要があります。 体系的に技術の学習が今後も必要 これはポートフォリオ作成ではなく、プログラミングを行う上での課題です。 よく言われている「プログラミングは暗記しなくていい」「まず手を動かしながら覚えていく」精神でポートフォリオ作成をしてきました。 わからないところはすぐにググって、都度実装してきたが、「実装したコードをちゃんと理解しているか」と自分に問うと、非常に怪しい・・・ コードを見直せば、何をしているのかはなんとなくわかるが、しっかり言語化するまでに時間がかかる。 体系的に理解できていないことがその原因だと考えています。 独学にこだわりすぎたのも問題で、メンターサービスなどの活用もするべきでした。 まとめ 当初考えていた機能は実装できたので、完成としました。 リファクタリングや機能の追加は今後も継続していきたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RancherOS のインストール

はじめに RancherOSについて 簡単に言えば、機能を絞ることで、メンテナンス性やセキュリティー性を高めているDocker用のLinuxです。コンテナ・オーケストレーションツールのRancherと名前が似ているので混同されるようですが別物です。 LinuxベースのOSで、サーバや仮想マシンにインストールして使用します。まず、よくあるUbuntu等との違いは、GUIが全く無いことです。ウィザードのようなインストール画面も存在しません。コマンド一つでインストールすることもできますが、慣れなければ何をどうしていいのかよく分かりません。 ネット上でもインストールのやり方を見つけることができますが、手順だけをなぞっているものが多いです。ゼロベースから読み解くのが大変だったので、自分でドキュメントをまとめてみました。 RancherOSのサイトはこちらです。 https://www.rancher.co.jp/rancher-os/ 詳しい設定の方法は以下のサイトから確認できます。 https://rancher.com/docs/os/v1.x/en/configuration/ インストールのフロー RancherOSをインストールする流れは、おおよそ以下のようになります。 メディアからRancherOSを起動 セットアップ用のファイルをアップロード インストールコマンドを実行してサーバにインストール RancherOSのインストールでは、Cloud-init という仕組みを使用しており、設定内容を記述したYAMLファイルを読み込ませてセットアップを行うことができます。この方法がRancherOSで想定しているインストール方法のようです。 ただ、そのYAMLファイルをどうやってアップロードするかという問題があります。一般的なLinuxのディストリビューションと違いセットアップウィザードが無いので、インストールの準備は手作業で行う必要があります。ネット上の情報では、この工程の情報が端折られていいるものが多かったです。 インストール 1. メディアからRancherOSを起動する インストール用のISOファイルを以下のページからダウンロードします。 https://github.com/rancher/os/releases/ rancher.iso をダウンロードしてください。慣れてきたら、仮想化基盤に合わせたISOを使ってインストールしたり、イメージをインポートして仮想マシンを作成しましょう。メディアから起動させるのには、何通りか方法があります。 CDやDVDを焼いてOSを起動させる 起動用のUSBメモリを作成して起動させる ネットワークから起動させる 現実的には、CDを焼くか起動用USBを作成して起動することになるでしょう。仮想マシンであれば、ISOイメージから直接起動させることもできます。これらの方法については、他のOSと同様なので、方法の説明は省きます。 問題は、メディアからRancherOSを起動させたあとです。メディアからRancherOSを起動させると、コンソールが立ち上がるだけです。インストールウィザードの類は実行されません。何も知らずにOSを起動すると、ここで何をすればいいか分からなくなります。 RancherOS は Linux がベースになっているので、busybox で使用できるような基本的なコマンドが実行できます。ネットワークの設定も、一時的なものであれば一般的なコマンドから設定することが可能です。vim は使えませんが、vi であれば実行できるので、設定ファイルの編集もできます。 2. セットアップ用のファイルをアップロード RancherOS のインストールはコマンド一つで完了します。シンプルなのでインストールウィザードが要らないというコンセプトのようです。セットアップ用のコマンドは、設定ファイル(Cloud-comfig.yml)を指定して実行するだけで完了できます。 ただし、この設定ファイルをアップロードするための手段が必要になります。インストールの際にメディアに保存してあるのなら、そこから読みだすことも可能でしょう。ただ、大概はネットワーク上からscpでコピーすることになります。設定ファイルをscpでコピーするために必要な設定項目は、 IPアドレスの設定 Default Gateway の設定 SSHのパスワードの設定 これらは、コマンドを使って手動で設定する必要があります。以下のコマンドでIPを振ることができます。 $ sudo ip address add 192.168.*.*/24 dev eth0 $ sudo route add default gw 192.168.*.* $ sudo passwd rancher # rancherユーザのパスワードを変更 # Pingも実行できます。 $ Ping 192.168.*.* この方法では設定が保存されないので、インストール後に再起動すると設定が消えてしまいます。セットアップ時の一時的な設定になります。ネットワークの疎通が取れてSSHでの接続が確認できたら、SCPで設定ファイルをアップロードしましょう。ユーザは rancher を使用します。 3. インストールコマンドを実行してサーバにインストール 設定ファイルが用意できたのであれば、あとはコマンドを実行するだけです。インストールの際にファイルシステムの設定を行う事もできるようですが、その場合は手動でのオペレーションが必要になるようです。インストールするドライブは、/dev 以下を見て確認します。仮想マシンの場合、sda ではなく vda になっていることがあります。 $ sudo ros install -c cloud-config.yml -d /dev/sda --append "rancher.password=<password>" コマンドを実行すれば、設定ファイルに事前に記載していた設定が反映された状態でOSがインストールされます。メディアを切断した状態で再起動後、password に設定したパスワードでログインできます。設定が反映されていればインストールは成功です。 インストール後に設定を変更したい場合、RancherOS の設定用コマンドから変更が可能です。一般的な Linux のようなコマンドや設定ファイルの編集による設定変更は対応していないので注意しましょう。 - 番外編 - 手早く使えるようにするための手順 メディアから起動した後に即インストールして、IPアドレスを設定すればRancherOSを利用できます。 $ sudo ros install -d /dev/sda --append "rancher.password=<password>" # 再起動後 sudo ros config set rancher.network.interfaces.eth0.address 192.168.*.*/24 sudo ros config set rancher.network.interfaces.eth0.gateway 192.168.*.* sudo ros config set rancher.network.interfaces.eth0.dhcp false ある程度慣れてきて、コマンドからの設定や、設定ファイルのマージができるようになったら試してみましょう。 設定項目について 必要になると思われる設定項目 RancherOS をインストールしてネットワークで通信できるようにするには、以下の設定が必要です。 Hostname IPアドレス Default Gateway DNS 検索するドメイン これらの項目を設定するための設定ファイルは、おおよそ以下のようになります。 hostname: **** # ホスト名 rancher: network: interfaces: eth0: address: 192.168.*.*/24  # IPv4 のアドレス gateway: 192.168.*.*  # Default Gateway のアドレス dhcp: false dns: nameservers: - XX.XX.XX.XX - XX.XX.XX.XX search: - example.com 設定項目は、あらかじめ決められた規則に乗っ取ってYAML型式で記述します。 eth0 のインターフェース名は、メディアから起動した際に ip a で確認できます。 詳しい設定方法については、こちらのリンクに記載されています。 https://rancher.com/docs/os/v1.x/en/networking/interfaces/ コマンドでの操作 RancherOS の設定には ros コマンドを使用します。設定については以下のコマンドで操作できます。 $ sudo ros config get <設定項目> # 設定内容を表示 $ sudo ros config set <設定項目> <設定値> # 設定を行う RancherOSでは、通常のLinuxと違い、サービスをコンテナで動かしています。サービスの状態は以下のコマンドで確認できます。 $ sudo ros service ps インストール後の設定ファイルによる設定 以下のコマンドで、設定ファイルの変更をマージして再起動します。 sudo ros config merge -i setting.yml sudo reboot 設定した内容は以下のコマンドでエクスポートできます。実行すると、現在の設定内容が設定ファイルの書式で表示されます。 $ sudo ros config export 時刻同期について RancherOSでは、System-Docker という仕組みでコンテナ上でNTPサーバを動かしています。Syste-Dokerで動いているNTPのコンテナの設定ファイルを編集して時刻同期を設定します。 ファイルの編集方法の例として、以下のページにやり方が記載されています。 https://rancher.com/docs/os/v1.x/en/configuration/write-files/ 設定ファイルに以下の内容を追記しておけば、System-Docker上で動いている NTP のコンテナの設定ファイルが編集されます。 #cloud-config write_files: - container: ntp # ココがコンテナ path: /etc/ntp.conf permissions: "0644" owner: root content: | server 0.pool.ntp.org iburst server 1.pool.ntp.org iburst server 2.pool.ntp.org iburst server 3.pool.ntp.org iburst # Allow only time queries, at a limited rate, sending KoD when in excess. # Allow all local queries (IPv4, IPv6) restrict default nomodify nopeer noquery limited kod restrict 127.0.0.1 restrict [::1]
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EC2でDockerを使っているとき、no space left on deviceとでたとき。

自作アプリに新たなgemをインストールして、ec2で docker-compose buildをしたとき、 no space left on deviceでた。 解決方法 まず、どのファイルが容量をくっているのかみました。 [myuser@ip-172-31-2-37 goal_tree]$ find . -xdev -type f | cut -d "/" -f 2 | sort | uniq -c | sort -n 1 .browserslistrc 1 .dockerignore 1 .env 1 .gitattributes 1 .gitignore 1 .ruby-version 1 .vscode 1 Dockerfile 1 Gemfile 1 Gemfile.lock 1 README.md 1 Rakefile 1 babel.config.js 1 config.ru 1 docker-compose.yml 1 dump.sql 1 entrypoint.sh 1 package-lock.json 1 package.json 1 postcss.config.js 1 storage 1 vendor 1 yarn.lock 1 クラス図.md 1 ポートフォリオ コンセプトの整理.md 1 機能設計.md 2 containers 2 lib 3 log 8 bin 18 db 27 test 37 config 59 public 87 app 303 .git 10375 tmp 16182 node_modules node_modulesがダントツで多い。 現段階では、.dockerignoreにnode_modulesを入れていなかったので、 buildするときに、node_modulesもbuildするようになっていたから、容量くっているのでは?という仮説を立てました。 .dockerignoreとは Before the docker CLI sends the context to the docker daemon, it looks for a file named .dockerignore in the root directory of the context. If this file exists, the CLI modifies the context to exclude files and directories that match patterns in it. This helps to avoid unnecessarily sending large or sensitive files and directories to the daemon and potentially adding them to images using ADD or COPY. イメージをbuildするときに、.dockerignoreに書いてあるものは除外してくれる。 メリットとしては、大きなファイルや機密性の高いファイルをデーモンに送信したり、イメージを追加することがなくなる。 今回の場合、node_modulesのように大きいファイルとかをデーモンに送信しなくてよくなるので、イメージの容量が減るかなと。 ですが、書きすぎると逆にbuildに時間がかかることもあるみたいです。 参考: .dockerignoreにnode_modulesをかいてみる .dockerignore tmp/* log/* .git containers ## ここを追記 node_modules 無事エラーが出ず、buildできるようになりました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CentOS8からUbuntu20.04に移行した話 (初期設定や録画環境構築)

前記事に書いたように、CentOS8 が晴れてEOLを迎えるためGWの有り余るヒマ時間貴重な休日を割いてUbuntuに移行した。二度とRHEL系は使わないからな!!バーカ!! 将来再度環境構築する際の自分用の備忘録として記載していく。 お品書き Ubuntuのインストールから初期設定まで 録画環境の構築(人様の記事を見ながら) Ubuntuのインストール 特に言及することはなく、Ubuntu20.04のisoをRufusでUSBに焼きこみ、インストール。 sdaにOS用のSSD、 sdb, sdcにデータ用のRaid1HDDをつないでいたので、sdaにインストールするよう設定。 参考: https://www.yokoweb.net/2020/05/04/ubuntu-20_04-lts-server-install/ Ubuntuのインストール後作業 サーバーの完全なシャットダウン・再起動 Ubuntuの再インストール後、 「完全に」 シャットダウンする。 sudo shutdownの後電源コードを物理的に抜き、5分放置、再度電源をつなげて起動。 この作業を行わなかったがためにチューナーがエラーを吐き、1日費やした。 IPアドレスの固定・DNS設定 ネットワーク内の別PCから操作するため。 IP固定は個人的にはルータ側でMACで指定するのが好み。 /etc/netplan/99_config.yaml network: version: 2 renderer: networkd ethernets: eth0: dhcp4: false dhcp6: false addresses: [192.168.xxx.yyy/24] gateway4: 192.168.xxx.1 nameservers: addresses: [192.168.xxx.1, 8.8.8.8, 8.8.4.4] sudo netplan apply 参考: https://qiita.com/zen3/items/757f96cbe522a9ad397d sshdの設定変更 セキュリティ向上のため 直接sshd_configを設定するのではなく、/etc/ssh/sshd_config.d/*.confを作成。 /etc/ssh/sshd_config.d/my_config.conf # 接続ポートの変更 Port XXXXX # ルートログインの禁止 PermitRootLogin no # 公開鍵認証の有効化 # パーミッションを .sshは600, authorized_keysは700に設定 PubkeyAuthentication yes AuthorizedKeysFile .ssh/authorized_keys # パスワード認証の制限(オプション) # PasswordAuthentication no # sshバージョンの制限 Protocol 2 sudo service ssh reload クライアント側では固めの鍵を作成しましょう。 ssh-keygen -t ecdsa -b 384 -f [鍵名] 参考: https://linuxliteracy.com/new-way-to-manage-ssh-options-in-ubuntu-20-04/ https://qiita.com/masa0x80/items/ecb692ad93f7d06a07b0 https://qiita.com/wnoguchi/items/a72a042bb8159c35d056 firewall無効化 より上位のルータで正しく制限されていることを確認の上実施。基本的には非推奨。 sudo ufw disable 参考: https://qiita.com/auto_masanori/items/3b4f60eb65d7db8ac48c AppArmor無効化 セキュリティの観点から無効化は非推奨。 sudo systemctl disable apparmor 参考: https://goto-linux.com/ja/2019/7/11/ubuntu-20.04-focal-fossa-linux%E3%81%A6apparmor%E3%82%92%E7%84%A1%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95/ rootのパスワード変更 Ubuntu初期インストールにおいてrootの初期パスはなし。 sudo passwd root パッケージマネージャの更新 sudo apt update sudo apt upgrade PostfixからGmail経由でメールを送信する 各種モニタリングの通知用。ウイルスチェック、RAID監視、SMART監視・・・ 参考: http://www.ckenko25.jp/2019/05/gmail-%E7%B5%8C%E7%94%B1%E3%81%A7%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92%E9%80%81%E4%BF%A1%E3%81%99%E3%82%8B%E3%82%88%E3%81%86%E3%81%AB-postfix-%E3%82%92%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B-for-ubuntu ClamAVのインストール セキュリティのため。毎日深夜に実行するよう設定 # インストール sudo apt install clamav clamav-daemon # ログ関係でエラーが出るので以下を設定 sudo rm /var/log/clamav/freshclam.log sudo touch /var/log/clamav/freshclam.log sudo chown clamav:clamav /var/log/clamav/freshclam.log # 設定ファイルの書き換え # old: create 640 clamav adm # new: create 640 clamav clamav sudo vi /etc/logrotate.d/clamav-freshclam # 定義更新 sudo freshclam # サービス起動の確認 sudo service clamav-freshclam status # 定期スキャンスクリプトの作成 sudo vi /root/script/clam-full.sh sudo chmod +x /root/script/clam-full.sh sudo mkdir /root/virus # 定期スキャン設定 sudo crontab -e # 0 2 * * * /root/script/clam-full.sh >> /var/log/clamav/clamav_scan.log /root/script/clam-full.sh #!/bin/sh echo ========================================= date hostname clamscan / \ --infected \ --recursive \ --log=/var/log/clamav/clamscan.log \ --move=/root/virus \ --exclude-dir=^/boot \ --exclude-dir=^/sys \ --exclude-dir=^/proc \ --exclude-dir=^/dev \ --exclude-dir=^/var/log/clamav/virus if [ $? = 0 ]; then echo "Virus not found." else echo "Virus found." cat /var/log/clamav/clamscan.log | mail -s "Server: Virus found" [RecievedErrorMail]@test.com fi 参考: https://www.yokoweb.net/2017/04/15/ubuntu-server-clamav/ Timezoneの設定 sudo timedatectl set-timezone Asia/Tokyo 参考: https://www.server-world.info/query?os=Ubuntu_20.04&p=timezone ntpを日本のものに変更 これをやらないと録画冒頭が10sec程切れて泣きを見る。 /etc/systemd/timesyncd.conf [Time] NTP=ntp.nict.jp FallbackNTP=ntp1.jst.mfeed.ad.jp ntp2.jst.mfeed.ad.jp ntp3.jst.mfeed.ad.jp systemctl restart systemd-timesyncd 参考: https://qiita.com/tukiyo3/items/c847d443a6d977083de9 PowerTopで省電力化 常に起動するサーバーなので省電力化する。 sudo apt install powertop tlp tlp-rdw sudo powertop --auto-tune sudo systemctl enable tlp sudo systemctl start tlp 参考: https://mumeiyamibito.0am.jp/linux_mint/%E9%AB%98%E5%BA%A6%E3%81%AA%E9%9B%BB%E6%BA%90%E7%AE%A1%E7%90%86 ファン制御 サーバーを寝室に設置しているのでファンを制御し静穏化する。 sudo apt install lm-sensors fancontrol sudo sensors-detect sudo service fancontrol stop sudo pwmconfig 参考: https://kledgeb.blogspot.com/2013/01/ubuntu-pwm-1.html mdadmで作成したRaid1ストレージの移行 [おま環]一度解除し、再度作成してresyncする必要があった?? # 一度解除し、再度raid1で設定 # 通常の環境ならこの作業は必要ないように思う。 # どうも前環境の時点でデグレ運用していた模様。気づいていなかった… mdadm --misc --stop /dev/md0 mdadm --create /dev/md0 --level=raid1 --raid-devices=2 /dev/sdb1 /dev/sdc1 # 設定の保存とresyncしているかの確認 mdadm --detail --scan > /etc/mdadm.conf cat /proc/mdstat fstabに設定して起動時マウント可能にする sudo blkidでuuidを確認 # /etc/fstab UUID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx /share auto defaults 0 0 参考: https://qiita.com/wnoguchi/items/b31e268b6b7236cdf8db 異常発生時にメール通知する /etc/mdadm/mdadm.conf ARRAY /dev/md0 metadata= .... # メール通知先を設定 MAILADDR [RecievedErrorMail]@test.com メールのテスト送信 mdadm --monitor --test --oneshot --scan docker, docker-compose インストール docker https://docs.docker.com/engine/install/ubuntu/ https://qiita.com/tkyonezu/items/0f6da57eb2d823d2611d docker-compose https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-compose-on-ubuntu-20-04-ja sambaでネットワークファイル共有(Docker, Docker-compose利用) dockerで環境を汚さず共有。録画ファイル等を別PCで見るために利用。 docker-compose.yml version: '2' services: samba: image: dperson/samba container_name: samba ports: - "139:139" - "445:445" volumes: - /path/to/share:/mnt/pub restart: unless-stopped command: "-s share;/mnt/pub;yes;no;no;samba;samba -u samba;<PASSWORD>" # ---Commandの説明--- # 設定詳細は https://hub.docker.com/r/dperson/samba を確認 # Windowsからの接続なのでパーミッションで悩まないAdminにする。 # 共有用のディレクトリを用意し、Ubuntu上のシステムファイル等を共有しない。 -s "<name;/path>[;browse;readonly;guest;users;admins;writelist;comment]" -u "<username;password>[;ID;group;GID]" 参考 https://hub.docker.com/r/dperson/samba 録画環境構築 前記事を記載した時点ではサーバーに直でインストールする方法の記事が多かったが、ここ1年でdockerで作成する方法が増えている。そもそもの本家MirakurunもDocker化しており、少し知識があれば誰でも手間なく録画サーバーを作成できるようになった。また、EPGStationもバージョンが上がりかなり使いやすくなっている。特に検索回りは楽になった。素晴らしい。 参考記事で丁寧に紹介されているため、参考記事を選定理由と共に述べるにとどめる。 こちらで紹介されている方法を使用した。 【完全解説】LinuxとPX-W3U4でEPGStation v2を構築する方法(PX-Q3U4/W3PE4/Q3PE4) この方法で動作することを確認。正常に動作している。 選定理由 px4_drvでmirakurun 3.5を利用可能。 Dockerfileがあり最悪自分での修正が容易、ただしコンテナ作成に結構な時間がかかる。 過去自分が利用していた方法に近い。 その他の方法 tv-recorder | https://github.com/collelog/tv-recorder たぶんこれが一番楽だと思います。 mirakurun 3.5を試してみたかったので選定から外した。 今回参考にした方法と組み合わせることで少量の改変でtv-recorderでも利用可能になりそう。 mirakcやハードウェアエンコが使えそうな雰囲気がある。 ラズパイ利用者やハードウェアエンコできる環境ならこちらの方がよい。 自分の場合、厳選したものを録画しCMカットして別のつよつよPCでソフトエンコしてるのでハードエンコの恩恵が薄い。 市販のTVレコーダーのようにとりあえず予約して見たら消すような使い方の人には重宝しそう。 あとがき 1年ちょっとでこれだけ快適に進化してるというのは驚いた。常に勉強することが一番の省力化なのだなと痛感するところです。 あと、スーパーカブの空気感が良い。みんなも録画しよう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む