20200209のdockerに関する記事は19件です。

別の Docker コンテナから mysqldump !!

ローカル環境で開発中に今のデータベースの状態保存しておきたい!
ってことたまにありますよね。

そんな時、汎用的に使えるコンテナ用意してみました :sunglasses:

MySQL の Docker コンテナの中でそれやればよくね?
っていうツッコミはご勘弁ください m(_ _)m 笑

デモ用のプロジェクト

こちら

ディレクトリ構成

operate_mysql_from_external
 - docker-compose.yml
 - .env
 - mysqldump
    - Dockerfile
    - mysqldump.sh
    - dumpfiles
       - 

docker-compose.yml

自動で .env ファイルを読み込んでくれるのは楽チンですよね。

ちなみにここで環境変数の受け渡しをしているのは、環境変数を設定せずに実行した時に下記のようなエラーを出してくれるからです。 docker-compose 最高ですね :relaxed:
You need to set the {環境変数名} environment variable.

version: '3'

services:
  mysqldump:
    build: mysqldump
    container_name: "demo-mysqldump"
    volumes:
      - './mysqldump/mysqldump.sh:/mysqldump.sh'
      - './mysqldump/dumpfiles:/dumpfiles'
    environment:
      MYSQL_HOST: "${MYSQL_HOST}"
      MYSQL_PORT: "${MYSQL_PORT}"
      MYSQL_USER: "${MYSQL_USER}"
      MYSQL_PASSWORD: "${MYSQL_PASSWORD}"
      MYSQLDUMP_OPTIONS: "${MYSQLDUMP_OPTIONS}"
      MYSQLDUMP_DATABASE_OPTION: "${MYSQLDUMP_DATABASE_OPTION}"
    command: sh mysqldump.sh

networks:
  dafault:
    external:
      name: "${MYSQL_NETWORK}"

参考
ネットワークの external 設定について
既存のネットワークを使う

「MySQL Docker コンテナが使用しているネットワーク名」については

docker network ls

を使って確認してみてください。

特別なことをしていなければ
{プロジェクト名}_default
もしくは
{プロジェクト名}_{ docker-compose.yml で指定しているネットワーク名}
となっているはずです!

.env

コンテナ内で使用する環境変数を記述しておきます。
以下はサンプルなので必要に応じて変更してください :robot:

MYSQL_HOST=mysql
MYSQL_PORT=3306
MYSQL_USER=user
MYSQL_PASSWORD=password

# MySQL Docker コンテナが使用しているネットワーク名
MYSQL_NETWORK=demo_mysql

MYSQLDUMP_OPTIONS=--quick --single-transaction
MYSQLDUMP_DATABASE_OPTION=demo

mysqldump

mysqldump する Docker コンテナ関連をまとめたディレクトリです。

Dockerfile

軽量の alpine に mysql-client を入れて使います。

FROM alpine:3.11

RUN apk update \
    && apk add mysql-client \
    && rm -rf /var/cache/apk/*

mysqldump.sh

mysqldump の処理を行うスクリプトです。

#!/bin/sh

# コマンドが失敗したら終了
set -e

echo "Domping for ${MYSQLDUMP_DATABASE_OPTION} from ${MYSQL_HOST}..."

readonly local OUTPUT_FILE="/dumpfiles/$(date +"%Y-%m-%dT%H%M%SZ").dump.sql"
readonly local MYSQL_HOST_OPTS="-h ${MYSQL_HOST} -P ${MYSQL_PORT} -u${MYSQL_USER} -p${MYSQL_PASSWORD}"

mysqldump ${MYSQL_HOST_OPTS} ${MYSQLDUMP_OPTIONS} ${MYSQLDUMP_DATABASE_OPTION} > "${OUTPUT_FILE}"

echo "Successfully dumped"

dumpfiles

dump したファイルたちがここに出力されます。

使い方

  1. MySQL の Docker コンテナが起動しているか確認
  2. .env に環境変数を設定する
  3. プロジェクトディレクトリで下記のコマンドを実行
# --rm を付けると実行後に自動でコンテナを削除してくれるよ
docker-compose run --rm mysqldump

おわりに

いかがでしょうか。
無事に mysqldump できましたか?
もし、こうした方がいいよ〜なんてことがあればコメントお待ちしています ( ^ ^ )/

参考

docker compose run について
mysqldump 公式リファレンス

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【DynamoDB】【Docker】docker-composeでDynamoDBとDjangoの開発環境を構築する

docker-composeでDynamoDBとDjangoの開発環境を構築する

はじめに

DynamoDBって安くて早くていいですよね。そんなDynamoDBをローカルで開発するためのDockerImageがあるってご存知ですか?そんなDockerImageを利用した開発環境構築の一例を照会します。

環境

プロダクション環境に即して利用するイメージのバージョンは以下です。
DynamoDBのGUI操作用にdynamodb-adminを利用します。Nodeはその環境を一緒に構築する際に利用します。

環境 バージョン
Python 3.7.4
MySQL 5.7
Node 10.16.3-alpine

環境構築

DjangoとMySQL

完全にゼロから構築するのでまずはDjangoプロジェクトを開始します。
公式のDjangoイメージはバージョンが古いので自分で作ります。

$ django-admin startproject dynamodb_example
$ cd dynamodb_example/
$ touch docker-compose.yml
$ touch Dockerfile
FROM python:3.7.4

RUN apt-get update
RUN apt-get install -y --no-install-recommends apt-utils gettext
RUN mkdir /app; mkdir /app/dynamodb_example

WORKDIR /app
COPY dynamodb_example /app/dynamodb_example
COPY requirements.txt /app/
COPY manage.py /app/

RUN pip install -r requirements.txt

EXPOSE 8080
CMD ["python", "manage.py", "runserver", "0.0.0.0:8080"]
docker-compose.yml
version: "3"
services:
  mysql:
    container_name: example-mysql
    ports:
      - 53306:3306
    image: mysql:5.7
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./persist/mysql:/var/lib/mysql
    restart: always
    environment:
      MYSQL_USER: example
      MYSQL_PASSWORD: example
      MYSQL_DATABASE: example
      MYSQL_ROOT_PASSWORD: example

  django:
    container_name: example-django
    build: .
    volumes:
      - .:/app
    working_dir: /app
    command: sh -c "./wait-for-it.sh db:3306; python3 manage.py runserver 0.0.0.0:8000"
    env_file: .env
    ports:
      - 58080:8000
    depends_on:
      - mysql

ポートが50000台なのは絶対被りたくないからです。とくに意味はありません。
DynamoDBと接続するためのライブラリはboto3とそれをWrapしているpynamodbを入れておきます。

requirements.txt
boto3
pynamodb
Django==2.2.4
djangorestframework==3.10.3
django-filter
mysqlclient==1.3.13

環境変数の意味をあまりなしていないですが一応…

DB_ENGINE=django.db.backends.mysql
DB_HOST=mysql
DB_DATABASE=example
DB_USERNAME=root
DB_PASSWORD=example
DB_PORT=3306

とりあえずDjangoとMySQLの疎通を確認したいのでDjangoのDATABASESを書き換えます。

setting.py
DATABASES = {
    'default': {
        'ENGINE': os.getenv('DB_ENGINE'),
        'NAME': os.getenv('DB_DATABASE'),
        'USER': os.getenv('DB_USERNAME'),
        'PASSWORD': os.getenv('DB_PASSWORD'),
        'HOST': os.getenv('DB_HOST'),
        'OPTIONS': {
            'init_command': 'SET foreign_key_checks = 0;',
            'charset': 'utf8mb4',
        },
    }
}

ここまで来たらとりあえず起動するか確認します。

$ docker-compose up -d
$ docker-compose exec django bash
$ python manage.py migrate

問題なければ、次にDynamoDB Localdynamodb-adminを入れてきます。

DynamoDB Local

docker-compose.ymlにdynamodbを記載します。
永続化ようにコマンドの最後にdbPathを指定します。

docker-compose.yml
  dynamodb:
    container_name: example-dynamodb
    image: amazon/dynamodb-local
    command: -jar DynamoDBLocal.jar -dbPath /home/dynamodblocal/data
    volumes:
      - ./persist/dynamodb:/home/dynamodblocal/data
    ports:
      - 50706:8000

dynamodb-admin

公式イメージがあったのですが、作ってしまったのでそちらを利用します。

$ mkdir dynamodb-admin
$ touch dynamodb-admin/Dockerfile
$ touch dynamodb-admin/.env
FROM node:10.16.3-alpine

RUN ["apk", "update"]
RUN ["npm", "install", "dynamodb-admin", "-g"]

EXPOSE 50727
CMD ["dynamodb-admin", "-p", "50727"]

環境変数のDYNAMO_ENDPOINTにはdynamodbのコンテナーサービス名とコンテナ側のポートを指定します。

DYNAMO_ENDPOINT=http://dynamodb:8000
AWS_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=ACCESS_ID
AWS_SECRET_ACCESS_KEY=ACCESS_KEY

このdyanamodb-adminを加えた最終的なdocker-compose.ymlファイルは以下のようになります。

docker-compose.yml
version: "3"
services:
  dynamodb:
    container_name: example-dynamodb
    image: amazon/dynamodb-local
    command: -jar DynamoDBLocal.jar -dbPath /home/dynamodblocal/data
    volumes:
      - ./persist/dynamodb:/home/dynamodblocal/data
    ports:
      - 50706:8000
  dynamodb-admin:
    container_name: example-dynamodb-admin
    build: dynamodb-admin/
    command: dynamodb-admin -p 8000
    env_file: dynamodb-admin/.env
    ports:
      - 50727:8000
    depends_on:
      - dynamodb
  mysql:
    container_name: example-mysql
    ports:
      - 53306:3306
    image: mysql:5.7
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./persist/mysql:/var/lib/mysql
    restart: always
    environment:
      MYSQL_USER: example
      MYSQL_PASSWORD: example
      MYSQL_DATABASE: example
      MYSQL_ROOT_PASSWORD: example
  django:
    container_name: example-django
    build: .
    volumes:
      - .:/app
    working_dir: /app
    command: sh -c "./wait-for-it.sh db:3306; python3 manage.py runserver 0.0.0.0:8000"
    env_file: .env
    ports:
      - 58080:8000
    depends_on:
      - mysql
      - dynamodb

http://localhost:50727をブラウザで表示して以下のように表示されていれば成功です。
スクリーンショット 2019-09-29 23.11.48.png
Create Table等したい放題です。

永続化の確認

適当にテーブルを作ってみます。
スクリーンショット 2019-09-29 23.18.51.png
作成されました。
スクリーンショット 2019-09-29 23.19.02.png

コンテナーを削除して再起動します。

$ docker-compose down
$ docker-compose up -d

http://localhost:50727を開いて先程のテーブルが残っていれば永続化もうまく行っています。

最後に

このリポジトリーは以下で公開します。今後更新するかもしれないのでtagは1.0.0です。
https://github.com/Cohey0727/example_dynamodb
もしよければ利用してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker を使って Djangoチュートリアルの Polls アプリを AWS ECS へデプロイするサンプルのようなもの

はじめに

こちらはDjangoのチュートリアル「はじめての Django アプリ作成、その1」のPolls(投票)アプリを Amazon Web Service(AWS) の Elastic Container Service(ECS) へデプロイするサンプル(のようなもの)です。

nginx + uwsgi + python + django + MySQL DB という構成で docker-compose を使っています。
また、デプロイには AWS CLI と ECS CLI を使い AWSコンソールは(ほとんど)使いません。(EC2インスタンス への ssh 接続でセキュリティグループを編集するときにだけコンソールを使用します)

前半はローカル環境上で docker-compose を使い Polls アプリを作成して動かすところまで。
後半で前半に作成した Polls アプリを ECS へデプロイします。

ソースは GitHub 上に公開しています。
(但しDjangoアプリの部分はこれから作るため含まれていません。)
https://github.com/Brokenumbrella/django-ecs-sample
この説明の中でも「1. 前準備」の中で上記から clone します。

ここでは説明しないこと

  • git や docker のインストール方法と使い方など。
  • AWS アカウントの取得方法や AWS の仕組みなど。
  • AWS CLI, ECS CLI のインストール方法や使い方など。

前提条件

  • git をインストールしている。
  • docker をインストールしている。
  • AWS のアカウントを持っている(フルアクセス出来るものが良いと思います)。
    もしAWSのアカウントが無くても前半のローカル環境で動かす所までは出来ます。
  • AWS CLI をインストールしている。
    インストール方法は後半でAWSサイトへのリンクを載せています。
    そこでインストールするのでも構いません。
    インストールしなくても前半のローカル環境で動かす所までは出来ます。
  • ECS CLI をインストールしている。
    インストール方法は後半でAWSサイトへのリンクを載せています。
    そこでインストールするのでも構いません。
    インストールしなくても前半のローカル環境で動かす所までは出来ます。
  • インターネットに接続できる。
  • 作るプロジェクトの名前は mysite です。
    (変更したい場合は Grep で mysite の文字列を全て変更してください)

Python は docker コンテナで構築するため事前にインストールしておく必要はありません。
と言いたい所ですが、AWS CLI が Python 2.7 もしくは 3.4 以降を使います。
AWS CLI をインストールする際は入っていなければ Python のインストールが必要です。

動作確認済みの環境

  • Ubuntu 18.04.3 LTS
    Docker version 19.03.5, build 633a0ea838
    docker-compose version 1.25.0, build 0a186604

  • Windows 10 Enterprise 1903 build 18362.418
    Docker version 19.03.5, build 633a0ea
    docker-compose version 1.24.1, build 4667896b
    (Docker for Windows を使用)

【注意】Windows ではコマンドの実行を Git-Bash 上で行って下さい。
PowerShell や コマンドプロンプト では id コマンドが使えず動きません。

構成

  • OS : Alpine3
  • 言語 : Python3(Python公式の DockerImage をベースにしています)
  • フレームワーク : Django2
  • WSGIサーバー : uwsgi
  • Webサーバ : nginx
  • データベース : mysql8

各インストールバージョン

  • Alpine 3.1.0
  • Python 3.7.4
  • Django 2.2.10 以上 3.0 未満
  • uwsgi 2.0.18 以上 3.0 未満
  • nginx 1.17.8
  • MySQL 8.0.19
  • mysqlclient 1.4.6 以上 2.0 未満

動作確認は上記の一番低い(確認時のリリース)バージョンで行っています。
Python と Alpine は Python の公式リポジトリにあればどれでも使う事が出来ると思います。
nginx の別バージョンを使いたい場合は ./docker-compose.yml と ./web/Dockerfile-nginx を編集して下さい。
MySQL の別バージョンを使いたい場合は ./docker-compose.yml と ./web/Dockerfile-mysql を編集して下さい。
Django, uwsgi, mysqlclient の別バージョンを使いたい場合は ./web/requirements.txt を編集して下さい。
※Django は3.0がリリースされていますが、テストできておらず 2.2 を対象としています。
※Python 3.8.1 + apine 3.11 ではDjangoアプリ作成後の動作確認だけで、アプリの作成はチェック出来ていません。多分動くのでは無いかと・・・

お約束

  • 個人的なテストとして作成したものでなんら保証が出来るようなものではありません。
  • 試される際は自己責任でお願いします。
  • 私自身の理解が足りておらず「コピペで動いている」ところもあります。
  • とはいえ、この情報が誰かの役に立てばいいなと思っています。

1. 前準備

開発用のソースを GitHub から取得(clone)します

1. プロジェクト用のフォルダを作り移動します

例)home フォルダに myprojects というフォルダを作成してそこに取得する場合。

shell
$ mkdir ~/myprojects/  && cd ~/myprojects/

このフォルダ下に django-ecs-sample というフォルダが作成されます。

2. GitHub からテストプロジェクトを clone します

shell
$ git clone https://github.com/brokenumbrella/django-ecs-sample.git

Windowsで git の改行コードの自動変換がONの場合には注意が必要です
git のインストール方法によっては、改行コードが CR+LF で取得されます。
もし web/run-my-app.sh ファイルを VSCode などのエディターで開いた際に改行コードが CRLF だった場合は、LF に変更して下さい。
linux では CRLF のシェルスクリプトファイルは実行できずエラーとなります。
(私はこれの解決に1日かかってしまいました)

3. clone したフォルダ及びファイルの構成

django-ecs-sample フォルダは下記の構成になっています。

+ db
  + conf
    - mysql_my.cnf                # MySQL8 で使う設定ファイル
+ nginx
  - uwsgi_params                  # uwsgi の設定ファイル
  + conf
    - default.conf                # nginx の設定ファイル
+ web
  - Dockerfile                    # docker-compose でビルドする際に使う Dockerfile
  - Dockerfile-mysql              # ECS へデプロイする際に使う MySQL用 Dockerfile
  - Dockerfile-nginx              # ECS へデプロイする際に使う nginx用 Dockerfile
  - Dockerfile-web                # ECS へデプロイする際に使う Web(Django)アプリ用 Dockerfile
  - requirements.txt              # Python のライブラリ定義ファイル
  - uwsgi.ini                     # uwsgi 用の設定ファイル(プロジェクトフォルダーへコピーします)
  - run-my-app.sh                 # ECS へデプロイする際に使う Webアプリ起動用シェルスクリプト
- .gitignore
- docker-compose.yml              # docker-compose 用ファイル
- docker-compose-ecs.yml          # ECS へデプロイする際の docker-compose ファイル
- docom.sh                        # docker-compose を簡単に利用する為のスクリプト
- esc-params.yml                  # ECS へデプロイする際のタスク定義ファイル
- readme.md                       # 簡単なドキュメント
+ log                             # ログ保存用のフォルダ
  + uwsgi
    - __init__.txt
+ static                          # staticファイル用のフォルダ
  - __init__.txt
+ src                             # プロジェクトを入れるソースフォルダ
  - __init__.txt

作成する Django アプリのソースファイルは ./src/ フォルダに置きます。
log, static, src はdocker-composeする際にマウントする際に必要になるため、Gitリポジトリ上では単なる空フォルダです。
gitで空フォルダを保持するために __init__.txt を入れています。

dockerコンテナ内でのユーザーの追加と変更について

dockerコンテナに(ホストマシンの)現在のユーザーを追加するため linux の id コマンドを使っています。
ユーザー切り替えを行う事で docker 側でマウントしたフォルダに現在のユーザーと同じ権限でファイルやフォルダを作成出来ます。
ユーザーを指定していない場合は root ユーザーでファイルが作られるため、ローカル環境で編集するには root への昇格が必要になります。
そういった手間を減らすためと、docker は root ユーザー以外で運用する方が良いらしいのでこのようにしています。
このため動作確認済みの環境以外では動かない可能性もあります。ご注意下さい。

docom.shについて

上記の id コマンドを使ったユーザーの設定などを簡略化するため docom.sh というスクリプトファイルを同封しました。
docker-compose を起動する際にユーザーID等をセットするためのシェルスクリプトで下記のようになっています。

shell
$ cat docom.sh
DUID=$(id -u) DGID=$(id -g) MYSQL_PW=PWRoot1 docker-compose $1 $2 $3 $4 $5 $6 $7 $8 $9

環境変数の DUID と DGID には id コマンドを使ってユーザーIDとグループID を入れています。
(ちなみに名前ではなく 1000 といった番号です)
また MYSQL_PW=PWRoot1 の部分は MySQL DB のルートユーザーパスワードになっています。
実際に使う際は変更して頂くようお願いします。(このままでも動きますが)
変更の際は web/mydb.cnf(プロジェクトフォルダへコピーしてればそちらも) と docker-compose-ecs.yml にも同じパスワードを設定して下さい。
"PWRoot1" を Grep で一括変換する方が良いと思います。

上記の設定を一時的に環境変数へセットし docker-compose を呼び出しています。
今後はコード簡便化のため、このスクリプトを使って説明していきます。
このスクリプトは開発やテストを目的としており、セキュリティに注意すべき環境では他の方法を検討して下さい。(ここ大事)

2.ローカル環境で Django Polls アプリを動かしてみよう

それでは本題に入りましょう。
docker compose を使いローカル環境で nginx, MySQL, Web(Djangoアプリ) の3つのコンテナを起動して動かし Polls アプリを作ります。

1. web 用の docker image を作成するためビルドします

まず Python+Django 環境を構築するために docker-compose build を行います。
下記コマンドを docom.sh ファイルのあるフォルダで実行します。

shell
$ ./docom.sh build web

初めてビルドする際には時間がかかります。
(ベースコンテナをダウンロードするためインターネットへの接続スピードにも影響されます)
下記のように Successfully built と出ればビルド成功です。

shell
Successfully built 036160743a81
Successfully tagged django-ecs-sample_web:latest

ここでビルドに使うファイルについて簡単に説明します。

(1)Web コンテナをビルドするための Dockerfile です

./web/Dockerfile
FROM python:3.7.4-alpine3.10
#  alpine では apk を使う(add でインストール)
# nginx, suprevisor, uwsgi のインストール(gcc,build-base,linux-headersはuwsgiインストール時に使うためインストールする)
# libffi-dev, mysql-dev, mysql-client, python3-dev は mysql を使うためにインストール
RUN apk update && apk add --no-cache \
    gcc \
    build-base \
    linux-headers \
    libffi-dev \
    mysql-dev \
    python3-dev && \
    pip3 install --upgrade pip

# requirements.txt から Django などの必要なライブラリをインストール
# 不要になった gcc などをアンインストール
COPY ./requirements.txt /code/
RUN pip3 install -r /code/requirements.txt && \
    apk del gcc build-base linux-headers libffi-dev python3-dev

# 8001番ポートを開放する(ことを宣言する)
EXPOSE 8001
# 作業用フォルダを /code/ にする
WORKDIR /code/

# ユーザーを作成してカレントユーザーにする(一般ユーザーで動かすため)
# docker-compose.yml の args に指定した uid と gid を使えるように宣言する
# ユーザー名、グループ名は id と一緒にしておく
ARG DUID
ARG DGID

# ユーザーの作成
RUN addgroup -S -g $DGID $DGID && \
    adduser -S -u $DUID -g $DGID $DUID

# ユーザーを切り替える
USER $DUID

# 実行コマンドは docker-compose.yml の方で指定するためここはシェルを指定しておく
CMD ["/bin/sh"]
  • python:3.7.4-alpine3.10 イメージを元にしています。
    DockerHubに公開されていれば、FROM python:3.7.4-alpine3.10 の部分を変更する事で別バージョンのPythonやalpineを使うことも出来ます。
    但しバージョンによってはテストアプリが動かない可能性もあります。
  • ライブラリのインストール時に必要な gcc 等をインストールしています。
  • nginx との接続用に 8001 番ポートを開けています。
  • /code/ を作業フォルダにしていますが、docker-compose.yml でローカル環境の ./src/ フォルダへマウントします。 つまりこのコンテナ内で /code/ 下に置いたファイルはローカル環境の ./src/ 下に置かれます。 (マウントについては dockerのドキュメント等をご参照下さい)
  • mysql-client は Djangoアプリから MySQL を使うのに必要がないためインストールしていません。
  • ユーザーIDとグループIDを環境変数から取得してユーザーを作成します。

(2)インストールする Python ライブラリを指定するファイルです。

./web/requirements.txt
django>=2.2.10, <3.0       # django 2.2.10 で動作確認済み、2.?.? の間は動くと仮定している
uwsgi>=2.0.18, <3.0       # uwsgi 2.0.18 で動作確認済み、2.?.? の間は動くと仮定している
mysqlclient>=1.4.6, <2.0  # mysqlclient 1.4.6 で動作確認済み、1.?.? の間は動くと仮定している

django, uwsgi, mysqlclient をインストールしています。
こちらで動作確認したのはそれぞれ >= で書かれているバージョンです。
もしアプリが起動しない場合、>= を == に変更し、,以降をコメントアウトする事でバージョンを固定してみて下さい。

例)
django==2.2.10

(3)docker compose で使用するYAMLファイルです。

docker-compose.yml
version: '3'

services:
  db:                                         # MySQL DB 用の設定(この db という名前で web から DB HOST の指定ができる)
    image : mysql:8.0.19
    container_name: mysql.db
    volumes:                                  # マウントフォルダの指定
      - ./db/data:/var/lib/mysql              # データの永続化を行う
      - ./db/conf/:/etc/mysql/conf.d/         # 設定ファイルをここから読み込ませる
      - ./db/sqls:/docker-entrypoint-initdb.d # 初期データを与える場合はここから読み込ませる
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_PW}       # rootパスワードの設定
      - MYSQL_DATABASE=mysite                 # 作成するDatabase名
      - TZ=Asia/Tokyo                         # タイムゾーンを日本時間に変更

  web:                                        # Web(Django)アプリケーション 用の設定
    build:                                    # ビルド設定
      context: ./web                          # ./web/Dockerfile を用いてビルドする
      args:                                   # Dockerfile へ渡す環境変数の指定
        - DUID=${DUID}
        - DGID=${DGID}
    environment:                              # 実行時に設定する環境変数
      - MYSQL_HOST=${MYSQL_HOST}
    user: ${DUID}:${DGID}                     # 実行時のユーザー指定
    container_name: django.web
    command: uwsgi --ini /code/mysite/mysite/uwsgi.ini
    volumes:                                  # マウントフォルダの指定
      - ./src:/code                           # アプリケーションのソースフォルダ
      - ./static:/static                      # static ファイルフォルダ
      - ./log/uwsgi/:/var/log/uwsgi           # uwsgi の設定ファイルをここから読み込ませる
    expose:                                   # 開放するポート
      - "8001"
    links:                                    # db コンテナを先に立ち上げてから web を立ち上げる
      - db

  nginx :                                     # nginx 用の設定
    image: nginx:1.17.8
    container_name: nginx
    ports:                                    # 8080 ポートを 80 ポートへ
      - "8080:80"
    volumes:                                  # マウントフォルダの指定
      - ./nginx/conf:/etc/nginx/conf.d        # nginx 用の設定ファイルをここから読み込ませる
      - ./nginx/uwsgi_params:/etc/nginx/uwsgi_params  # uwsgi のパラメータファイルをここから読ませる
      - ./static:/static                      # static ファイルフォルダ
      - ./log/nginx/:/var/log/nginx           # ログファイルをここへ保存する(永続化)
    depends_on:
      - web                                   # web コンテナの後から起動させる

2. Django project を作成します

上記 1. で作った docker 環境を使いプロジェクトを作成します。
django-admin startproject コマンドを使い mysite という名前で新規作成します。
(mysite 以外の名前にする場合は MySQL DB のテーブル名等も変更する必要があり、 grep 検索などで置換して下さい。)

下記コマンドを実行してプロジェクトを作成します。

shell
$ ./docom.sh run web django-admin startproject mysite

コンテナ内で /code/ フォルダに、ローカル環境では ./src/ フォルダにプロジェクトが作成されます。
もし ./src/mysite/ フォルダが作成されていない場合はMySQLサーバーの立ち上げで失敗している可能性があります。
db/data/ フォルダのファイルとサブフォルダを全て削除してやり直してみて下さい。

3. ./web/uwsgi.ini ファイルを ./src/mysite/mysite/ へコピーします

ローカル環境で下記コマンドを使ってコピーします。

shell
$ cp ./web/uwsgi.ini ./src/mysite/mysite/

これは uwsgi 用の設定ファイルです。
コピーする事でコンテナ内では /code/mysite/mysite/uwsgi.ini に配置される事になります。

./web/uwsgi.ini
[uwsgi]
# この prjname に django-admin startproject で作成したプロジェクト名を指定します。
prjname=mysite
basepath=/code/%(prjname)/
chdir=%(basepath)
module = %(prjname).wsgi:application
socket = :8001
wsgi-file = %(basepath)%(prjname)/wsgi.py
logto = /var/log/uwsgi/uwsgi.log
py-autoreload = 1
# usage:
#  このファイルは django-admin startproject の後に /code/prjname/prjname/ へコピーする。

4. nginx の設定ファイルについて

これは nginx 用の設定ファイルです。
このファイルは nginx コンテナを実行する際にマウントさせて読み込ませるためコピーなどは必要ありません。(こんなファイルだという説明だけです)

./nginx/conf/default.conf
upstream django {
  ip_hash;
  server web:8001;
}
server {
  # the port your site will be served on
  listen      80;
  server_name localhost compute.amazonaws.com; # substitute your machine's IP address or FQDN
  charset     utf-8;
  client_max_body_size 75M;   # adjust to taste
  location /static {
    alias /static;
  }
  location / {
    include     /etc/nginx/uwsgi_params; # the uwsgi_params file you installed
    uwsgi_pass  django;
  }
}
  • django との接続に web コンテナの 8001 ポートを使うように指定します。(ここは理解不足です)
  • server_name では localhost と EC2 の 2つを指定しています。実際にはドメインやIPアドレスを指定するようにして下さい。
  • location /static{ alias } で /static へのアクセスを nginx コンテナの /static フォルダにマッピングさせています。
    ここは後ほど collectstatic で /static フォルダに集約させます。
  • location /{} で /static 以外のアクセスを全て django に処理させるようにしています。

5. MySQL DB を使うように設定を行います。

Django はデフォルトで DataBase に sqlite3 を使うようになっています。
ここでは MySQL DB を使わせるように設定を変更します。

  • ./src/mysite/mysite/settings.py を編集します。
./src/mysite/mysite/settings.py
DATABASES = {
  'default': {
    'ENGINE': 'django.db.backends.sqlite3',
    'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
  }
}

上記の部分を下記のように書き換えます。

./src/mysite/mysite/settings.py
DB_SETTING_FILE = os.path.join(BASE_DIR,'mydb.cnf')
DATABASES = {
  'default': {
    'ENGINE': 'django.db.backends.mysql',
    'OPTIONS':{
      'read_default_file':DB_SETTING_FILE,
    },
  }
}

DB ENGINE には django.db.backends.mysql を指定します。
MySQL 関連の設定は次に説明する mysql.cnf ファイルから読み込むように指定しています。
ついでに、Djangoで使う文字コードを日本語に、時間も日本時間へ変更しておきます。
LANGUAGE_CODE を 'ja' に、TIME_ZONE を 'Asia/Tokyo' に変更します。

./src/mysite/mysite/settings.py
LANGUAGE_CODE = 'ja'

TIME_ZONE = 'Asia/Tokyo'
  • ./web/mydb.cnf を ./src/mysite/ へコピーします。
    Django から MySQL DB へアクセスするための設定をこのファイルに纏めています。 コードは下記のようになっています。
./web/mydb.cnf
[client]
  database = mysite
  user = root
  password = PWRoot1
  host = db
  port = 3306
  default-character-set = utf8mb4
  • 'database' には docker-compose.yml の service: db: environment: MYSQL_DATABASE に指定したDataBase名を入れます。
  • 'user' は root ユーザーでアクセスさせています。
  • 'password' には docom.sh で指定した MYSQL_PW の値を指定して下さい。
  • 'host' には docker-compose.yml で定義した service の名前 'db' を指定します。後は docker compose がよしなにやってくれます。
  • 'port' は 一般的なMySQLで使用するポート番号 3306 を設定しています。
  • 'default-character-set' で絵文字などに対応したutf8mb4 を指定しています。

6. docker-compose up でサーバーを立ち上げてアプリケーションを動かします

下記コマンドでサーバーが起動します。
(-d を指定してバックグラウンドでコンテナを実行させています。)

shell
$ ./docom.sh up -d
Starting mysql.db ... done
Starting django.web ... done
Starting nginx      ... done

上記のように3つのコンテナが起動したら、Webブラウザを立ち上げて http://localhost:8080 にアクセスします。
下記の Django のデモ画面が表示されれば問題なくサーバーが起動できています。
おめでとうございます!
django-ecs-sample01.png

7. docker-compose down サーバーを終了します

下記コマンドを実行します。

shell
$ ./docom.sh down
Stopping nginx      ... done
Stopping django.web ... done
Stopping mysql.db   ... done
Removing nginx      ... done
Removing django.web ... done
Removing mysql.db   ... done
Removing network django-ecs-sample_default

上記のようにコンテナが停止・削除されます。
これ以降は http://localhost:8080 へアクセスしてもエラーが返されます。

8. ログファイルはローカル環境の ./log/ フォルダ下に保存されています

サーバーが起動しない等の場合は下記フォルダのログを見て原因を調べることが出来ます。

./log/nginx/ : nginx が出力するログ
./log/uwsgi/ : uwsgi が出力するログ

9. Django アプリケーションを作ります

ここで作るアプリケーションはチュートリアルの polls です。
また以下2つの方法があります。どちらでも好きな方法で作る事が出来ます。

  • 方法1:サーバーを起動させて docker exec によりコンテナ内に入って作業する

  まずサーバーを起動していない場合は起動させます。

shell
$ ./docom.sh up -d

  サーバーが立ち上がったら docker exec で django.web に shell で接続します。
  (Windows10 の場合は winpty をつけて下さい)

shell
$ docker exec -it django.web sh
※windows10 の場合
$ winpty docker exec -it django.web sh

/code $ とプロンプトが出ればコンテナ内に入れています。
  misite プロジェクトフォルダ上で python manage.py startapp を実行します。

shell
/code $ cd mysite
/code $ python manage.py startapp polls
/code $ exit

  ※exit でコンテナから抜けます。

  起動したサーバーを停止します。

shell
$ ./docom.sh down
  • 方法2:docker-compose run web で作る場合

アプリケーションの作成は manage.py のあるフォルダで行うためワークフォルダを指定する必要があります。
docker-compose run -w //code/mysite/ で指定できるので、これを使います。
(ubuntu では /code/mysite/ でも動いたのですが、Windows10 では //code/mysite/ でなければ動きませんでした。)

shell
$ ./docom.sh run -w //code/mysite/ web python manage.py startapp polls

どちらの方法でもアプリケーションを作成できます。
ローカルに ./src/mysite/polls フォルダが出来ていればアプリケーションの作成は成功です。

10. アプリケーションへアクセスできるようにビューとurlsを指定します

ここからは Django の Polls チュートリアルとほぼ同様のコーディング作業になります。

(1) ./src/mysite/polls/views.py を下記のように編集します。

./src/mysite/polls/views.py
from django.http import HttpResponse

def index(request):
    return HttpResponse("Hello, world. You're at the polls index.") 

(2) ./src/mysite/polls/ フォルダに urls.py ファイルを作成し、下記コードを書きます。

./src/mysite/polls/urls.py
from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

(3) 次に ./src/mysite/urls.py ファイルを下記のように書き換えます。

./src/mysite/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]

このルーティングによって http://localhost/polls/ へアクセスする事で 1 の inxex(request) が呼び出されレスポンスが返されるようになります。

11. ここまで出来た Polls アプリを動かしてみましょう

下記コマンドでサーバーを立ち上げます。

$ ./docom.sh up -d

Webブラウザで http://localhost:8080/polls へアクセスします。
URL には /polls を付けて下さい。先ほどと同じlocaphost:8080だけでは Page not found(404) エラーが出ます。
ブラウザ画面に

  Hello, world. You're at the polls index.

と表示されていればここまでは成功です。
サーバーを停止させます。

shell
$ ./docom.sh down

12. collectstatic でスタティックファイルを所定のフォルダへ集めます

AWS ECS などへ公開する場合は Javascript や css 、画像などの static ファイルを1箇所にまとめる必要があります。
また、今回 nginx で /static/ をホストしているためここで纏めておきます。
これらのファイルを纏めるために django では collectstatic が用意されています。
collectstatic を実行するには前準備をする必要がありますので、そこから説明します。

(1). はじめに ./src/mysite/mysite/settings.py の最後に STATIC_ROOT の設定を追記します。
これを入れ忘れると collectstatic で下記のエラーが出ますので、これが出たらここに戻って確認して下さい。

django.core.exceptions.ImproperlyConfigured: You're using the staticfiles app without having set the STATIC_ROOT setting to a filesystem path.

では下記のように追記します。

./src/mysite/mysite/settings.py
  |
  〜いろいろ〜
  |
  STATIC_URL = '/static/'
  STATIC_ROOT = STATIC_URL      # これを追加する

(2). collectstatic を行います。
サーバーが立ち上がっていなければ起動します。

$ ./docom.sh up -d

docker exec でコンテナに入ります。(ubuntu の場合)

$ docker exec -it django.web sh

(Windows10 の場合は winptyをつけて)

$ winpty docker exec -it django.web sh

/code $ とプロンプトが出ればコンテナ内に入れています。

/code $ cd mysite
/code $ python manage.py collectstatic
/code $ exit

もしくは docker-compose run を使い以下の1行でも作成可能です。

$ ./docom.sh run -w //code/mysite/ web python manage.py collectstatic

./static フォルダにファイルが存在する場合は途中で下記のような問いが出ますので、yes を入力して下さい。

This will overwrite existing files!
Are you sure you want to do this?

Type 'yes' to continue, or 'no' to cancel:

./static フォルダに admin フォルダが追加されている事を確認できると思います。

13. Djangoチュートリアルに従い Polls アプリを作成します

ここからは、下記公式サイトのチュートリアルのその2から7までを実際に行います。
(「高度なチュートリアル」はやらなくても大丈夫です。)
はじめての Django アプリ作成、その2 モデルの作成

チュートリアル内で python manage.py 〜 という部分が出てきた際は「9.Django アプリケーションを作ります」と同じように docker exec もしくは docker-compose run を使います。
例えば python manage.py makemigrations polls を行う場合下記の手順で実行してください。
サーバーが立ち上がっていなければ起動します。

$ ./docom.sh up -d

ubuntu の場合

$ docker exec -it django.web sh

Windows10 の場合

$ winpty docker exec -it django.web sh

/code $ とプロンプトが出ればコンテナ内に入れています。

/code $ cd mysite
/code $ python manage.py makemigrations polls
/code $ exit

./docom.sh up -d はサーバーを起動してない場合に必要です。起動していれば docker exec ~ から実行してください。
またサーバーを起動しておけば、途中でもブラウザの表示更新(リロード)するだけ変更が適用されますので都度サーバーの起動、終了を行う必要はありません。
もしくは

$ ./docom.sh run -w //code/mysite/ web python manage.py makemigrations polls

でも行うことができます。
ただし残念ながら、superuser を作るコマンド python mange.py createsuperuser だけは docker-compose run では実行できません。ここではユーザー名などの入力を求められるからです。このため createsuperuser を行う際は docker exec 〜 を使って下さい。
それから、python shell を実行させている際にソースを修正しても起動している shell には反映されません。
shell を起動しなおす必要があります。
(起動しなおしたら必要であれば from datetime などの初期化処理からやり直します)

ここを完了しなくても次の「2.AWS ECS へデプロイしよう」へ進みデプロイする事は出来ます。
"Hello, world. You're at the polls index."と出るだけですが・・・

2.AWS ECS へデプロイしよう

1. AWS 側の準備

(1) Python をインストールします

AWS CLI をインストールするに先立ち、Python 2.7以降か 3.4 以降が必要になるためインストールします。
既にインストール済みの場合は 2. へ進んでください。
もし Python がインストールされているかわからない場合は、下記コマンドで確認出来ます。

$ python --version

Python 3 の場合は

$ python3 --version

バージョン番号が出ればインストールされています。(但し 2.7 or 3.4 未満の場合はアップデートが必要です)
ここでは Python3 の最新版をインストールします。
Python公式ページhttps://www.python.org/からダウンロード、インストールします。
Windows10 へインストールする場合はこちらも参考にさせていただきました。
Python3のインストール
今回は最新安定板の3.8.1をインストールしました。
インストールが完了すると下記のようにして確認できます。

$ python --version
Python 3.8.1

(2) AWS CLI のインストールと設定

AWS CLI の詳細は下記を参照してください。
AWS コマンドラインインターフェイス

インストールと設定は下記公式ページの手順で行ってください。
2020年2月現在では バージョン2は評価版での公開のため、バージョン1の方をインストールします。

AWS CLI のインストール

Windows10 では下記のページから MSI インストーラをダウンロードしてインストールしました。
また、Windows10では下記のドキュメントに従い PowerShell を使います。
https://docs.aws.amazon.com/ja_jp/cli/latest/userguide/install-windows.html#install-msi-on-windows

続けて設定を行います。
AWS CLI の設定

これ以降の説明では「AWS CLI のかんたん設定」の aws configure を実行したとして進めます。
もし「複数のプロファイルの作成」で profile 名を指定した場合は aws --profile profuser ecr 〜〜 とプロファイル名を指定してコマンドを実行してください。(profuser の部分には作成したプロファイル名を指定します)

(3) ECS CLI のインストールと設定

ECS CLI の詳細は下記を参照してください。
AWS ECS コマンドラインインターフェースの使用
インストールは下記公式ページの手順で行ってください。
現時点では Version2 はまだ評価版なので、Version1 をインストールします。
また、ここでは下記のドキュメントに従い Windows10 では PowerShell を使います。

Amazon ECS CLI のインストール

ここも公式にお任せでOKかと思ったのですが、Windows でステップ2の「MD5 サムを使用した検証」をやる場合は注意が必要です。
(オプションなので飛ばす人はこの注意も必要ないです)
まずは手順通りに実行してみます。

ps c:\> Get-FileHash ecs-cli.exe -Algorithm MD5
Resolve-Path : パス 'C:\ecs-cli.exe' が存在しないため検出できません。
~云々~

のように、ecs-cli.exe ファイルが存在しないとエラーが出ました。
これは、ステップ1で C:\Program Files\Amazon\ECSCLI フォルダに ecs-cli.exe をダウンロードしているためです。
このためフルパスを指定して実行します。

PS C:\> Get-FileHash "C:\Program Files\Amazon\ECSCLI\ecs-cli.exe" -Algorithm MD5

これでハッシュ値がとれたと思います。
また、確認のためダウンロードした 'md5.txt' ファイルは確認が済めば不要なので削除して構いません。

2. docker image をビルドします

(1) ./src/mysite/mysite/settings.pyを修正します

ALLOWED_HOST に何処でも動作するよう '*' を指定しておきます。
運用するサーバーが決まったらここには hogehoge.com や '100.100.100.100' などドメインやIPアドレスを指定します。
また、実運用の際には DEBUG = False にしましょう。

./src/mysite/mysite/settings.py
  〜いろいろ〜

  ALLOWED_HOSTS = ['*']       # ここ

  〜いろいろ〜

(2) MySQL, nginx, web の設定やソースコードを含めた Dockerfile について説明します。

前半のローカル環境では MySQL, nginx の設定ファイルを docker の volumes を使って指定しましたが、ECS上で動かすにはコンテナ内にそれらも含めます。
また web の docker image には作成したアプリケーションのソースファイル(./src/ フォルダ以下)を含める必要もあります。
そのため、デプロイ用に web, MySQL, nginx 用 の Dockerfile を作成し、それぞれにビルドします。
各 Dockerfile は ./web/ フォルダに入っていますが、下記のようになっています。

./web/Dockerfile-mysql
FROM mysql:8.0.19

# 設定ファイルをコピーする
COPY ./db/conf/ /etc/mysql/conf.d/
./web/Dockerfile-nginx
FROM nginx:1.17.8

# 設定ファイルをコピーする
COPY ./nginx/conf /etc/nginx/conf.d
COPY ./nginx/uwsgi_params /etc/nginx/uwsgi_params
# static ファイルは nginx で返すためこのコンテナにコピーしておく
COPY ./static /static
./web/Dockerfile-web
FROM python:3.7.4-alpine3.10

# alpine では apk を使う(add でインストール)
# nginx, suprevisor, uwsgi のインストール(gcc,build-base,linux-headersはuwsgiインストール時に使うためインストールする)
# libffi-dev, mysql-dev, python3-dev は mysql を使うためにインストール
RUN apk update && apk add --no-cache \
  gcc \
  build-base \
  linux-headers \
  libffi-dev \
  mysql-dev \
  python3-dev && \
  pip3 install --upgrade pip

# requirements.txt から Django などの必要なライブラリをインストール
# 不要になった gcc などをアンインストール
COPY ./web/requirements.txt /code/
RUN pip3 install -r /code/requirements.txt && \
    apk del gcc build-base linux-headers libffi-dev python3-dev

# ソースファイルを /code/ フォルダへコピーする
COPY ./src/ /code/
# 実行時のスクリプトファイルをコピーする
COPY ./web/run-my-app.sh /code/

# 8001番ポートを開放する
EXPOSE 8001
# 作業用フォルダを /code/ にする
WORKDIR /code/

# ユーザーを作成してカレントユーザーにする(一般ユーザーで動かすため)
# docker-compose.yml の args に指定した uid と gid を使えるように宣言する
# ユーザー名、グループ名は id と一緒にしておく
ARG DUID
ARG DGID

# ユーザーの作成
RUN addgroup -S -g $DGID $DGID && \
    adduser -S -u $DUID -g $DGID $DUID

# uwsgi 用のログパスを追加
RUN mkdir /var/log/uwsgi/
RUN chown -R $DUID:$DGID /var/log && \
    chown -R $DUID:$DGID /code

USER $DUID

(3) 3つのDockerfileをビルドして docker image を作ります

今回は docker コマンドの build を使います。
前半の docker-compose ではないため docom.sh は使いません、ご注意ください。
下記のコマンドでビルドを行います。

$ docker build -t django-ecs-sample-web -f ./web/Dockerfile-web --build-arg DUID=$(id -u) --build-arg DGID=$(id -g) .
$ docker build -t django-ecs-sample-mysql -f ./web/Dockerfile-mysql .
$ docker build -t django-ecs-sample-nginx -f ./web/Dockerfile-nginx .
  • -t django-ecs-sample-web など、それぞれのイメージに対して今後利用しやすいように名前をつけています。
  • -f でビルドに使う Dockerfile を指定しています。
  • --build-arg DUID=$(id -u) --build-arg DGID=$(id -g) の部分はdocom.shで環境変数をdocker-composeへ渡していたのと同じです。 docker build では --build-arg オプションで環境変数を渡すところに注意が必要です。

3. docker-compose-ecs.yml について

ecs へデプロイするために専用ファイルを用意しました。
また各 image には仮の URL を指定しています。
後ほど ECR にリポジトリを作成して、そのURLに書き換えます。

docker-compose-ecs.yml
version: '3'

services:
  db:
    image : XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest
    volumes:
      - /db/data:/var/lib/mysql              # データの永続化を行う
    environment:
      - MYSQL_ROOT_PASSWORD=PWRoot1
      - MYSQL_DATABASE=mysite
      - TZ=Asia/Tokyo

  web:
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest
    command: sh /code/run-my-app.sh
    links:
      - db

  nginx :
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest
    ports:
      - "80:80"
    links:
      - web

この中で webコンテナの起動コマンドを下記のように変えています。
web: command: sh /code/run-my-app.sh
これは django の migrate で DB へのマイグレーション処理を行ってから uwsgi を起動するスクリプトです。
初回起動時に MySQL の準備が間に合わず migrate に失敗する事から成功するまで無限ループさせています。
無限ループは正直恐ろしいんですが、これに失敗するって事は MySQL が立ち上がらないということなので、いいかなと。
実際には他にもっとスマート(本来の)やり方があるんじゃないかと思います。
そもそも RDS などのマネージドサービス使えば前もって起動させておき、migrate なども別のタイミングで行うことが出来るはず、などなどのご意見もあると思いますが。
どなたかこの場合に他に何か良策があれば教えて頂けると助かります。
ともかく先へ進めるため下記のスクリプトを動かすようにしました。

./web/run-my-app.sh
#!/bin/sh

app=mysite
while :
do
  if python /code/$app/manage.py migrate; then
    break
  else
    sleep 1
  fi
done
uwsgi --ini /code/$app/$app/uwsgi.ini

exit 0

4. AWS ECR にリポジトリを作成し、dockerイメージをプッシュします

ECS へデプロイする際 docker image は ECR(Elastic Container Registry) か docker hub に置く必要があります。
今回は AWS で完結させたいので、先程ビルドした3つの docker image を AWS の ECRへプッシュします。

(1) AWS ECR にプッシュ用のリポジトリを作成します

始めに下記コマンドで、web 用の django-ecs-sample-web リポジトリを作成します

shell
$ aws ecr create-repository --repository-name django-ecs-sample-web

作成に成功すれば下記のようにJSON形式で作成したリポジトリの情報が返されコンソール画面に表示されます。

json
{ 
  "repository": {
    "repositoryArn": "arn:aws:ecr:ap-northeast-1:XXXXXXXXXXXX:repository/django-ecs-sample-web",
    "registryId": "XXXXXXXXXXXX",
    "repositoryName": "django-ecs-sample-web",
    "repositoryUri": "XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web",
    "createdAt": 1575769798.0,
    "imageTagMutability": "MUTABLE",
    "imageScanningConfiguration": {
      "scanOnPush": false
    }
  }
}

この時に返される"repositoryUri"の"XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web"を控えておきます。
もしくは、URL の構成は
[AWS ACCOUNT ID].dkr.ecr.[reagion].amazonaws.com/[repository name]
のようになっているので、アカウントID, リージョン名, リポジトリ名 から導き出すことも出来ます。(今回のリポジトリ名は django-ecs-sample-web です。)
これは push 用のタグ付けや docker-compose-ecs.yml でイメージの読み込み先として使うため後々必要になります。
続いて nginx、mysql 用のリポジトリも作成し、同様にrepositoryUriを控えます。

shll
$ aws ecr create-repository --repository-name django-ecs-sample-nginx
$ aws ecr create-repository --repository-name django-ecs-sample-mysql

(2) ./docker-compose-ecs.yml ファイルを修正します

リポジトリが出来たので、./docker-compose-ecs.yml ファイルの image 項目をリポジトリ URI に書き換えます。
./docker-compose-ecs.yml を開くと下記の用に XXXXXXXXXXXX.dkr.ecr となっていますので、ここを(1)で控えた URI に書き換えてください。(もしくはデプロイするアカウントIDをXXXXXXXXXXXXに入れるので構いません。)

./docker-compose-ecs.yml
  mysql:
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest

  web:
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest

  nginx:
    image: XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest

(3) ECR へプッシュできるよう docker image にタグを付けます

下記のように docker tag コマンドを使用します。
XXXXXXXXXXXX の URI 部分は、リポジトリ作成時に控えた repositoryUri の値を指定して下さい。

shell
  $ docker tag django-ecs-sample-nginx:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest
  $ docker tag django-ecs-sample-web:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest
  $ docker tag django-ecs-sample-mysql:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest

タグが付いたかは下記のようにして確認出来ます。

  $ docker images
  REPOSITORY                                                                TAG       IMAGE ID        CREATED       SIZE
  XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx   latest    1dd8c2bc8f1d    1 days ago    127MB
  django-ecs-sample-nginx                                                     latest    1dd8c2bc8f1d    1 days ago    127MB
  XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web     latest    7633e0977266    1 days ago    462MB
  django-ecs-sample-web                                                       latest    7633e0977266    1 days ago    462MB
  XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql   latest    fdbaa71f3406    1 days ago    456MB
  django-ecs-sample-mysql                                                     latest    fdbaa71f3406    1 days ago    456MB

(4) ECR リポジトリにへプッシュします

下記のように ecs-cli push コマンドを使います。
XXXXXXXXXXXX の URI 部分は、リポジトリ作成時に控えた repositoryUri の値を指定して下さい。

shell
$ ecs-cli push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest
INFO[0000] Pushing image       repository=XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx tag=latest
INFO[0015] Image pushed

のように表示されれば成功です。
また下記はインターネット接続を切った時に出たエラーメッセージです。
何らかのエラーが出るとこのように表示されるので適宜対応してください。

FATA[0000] Error executing 'push': unable to create repository: RequestError: send request failed
caused by: Post https://api.ecr.ap-northeast-1.amazonaws.com/: dial tcp: lookup api.ecr.ap-northeast-1.amazonaws.com: no such host

続けて nginx と mysql のコンテナも push します。

  $ ecs-cli push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest
  $ ecs-cli push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest

5. AWS ECS にクラスターを作成します

Amazon ECS クラスターとは、タスクまたはサービスの論理グループです。
EC2 を使用してタスクまたはサービスを実行している場合、クラスターはコンテナインスタンスのグループ化でもあります。
とのことですが、私は単純にインスタンス、サービス、タスクの容れ物だと思って使っています。
詳細は下記の公式サイトを参照して下さい。
Amazon ECS クラスター

(1) ecs-params.yml ファイルを作成します

Amazon ECS タスク定義にはdocker-compose.ymlには対応しないフィールドがあり、それを ecs-params.yml で指定します。
今回はリポジトリ内に用意しているためそれを使います。
ここでは無料枠があれば無料となる t2.micro マシンを使いたいのでメモリーサイズを調整しています。
このファイルが無い場合メモリーサイズは各コンテナに 500MB ずつ割り当てられるようです。
つまり3つコンテナを立ち上げるには 1.5GB 以上のメモリーを持ったEC2インスタンス(t3.smallやt2.smallなど)が必要になります。
t2.micro は残念がら 1GB しかメモリーがないので調整が必要ということです。
また、逆にもっと大きなメモリーサイズのEC2インスタンスを立ち上げて大量のメモリーを割り当てる場合にも設定が必要です。
但し下記は t2.micro で動かせたというだけで最適化は行っていません。

ecs-params.yml
version: 1
task_definition:
  services:
    nginx:
      mem_limit: 150m
      mem_reservation: 128m
    web:
      mem_limit: 448m
      mem_reservation: 400m
    db:
      mem_limit: 448m
      mem_reservation: 400m

ファイル名が ecs-params.yml なら自動的に読み込まれますが、他の名前を付ける場合は、--ecs-params にパス名を指定します。
詳細は下記の公式ドキュメントを参照して下さい。
Amazon ECS パラメータの使用

(2) ECS クラスター設定を作成します

この設定ではクラスター用のインスタンスの種類などを指定します。
ECS CLI を使い下記のコマンドを実行する事でクラスター設定を作成できます。

$ ecs-cli configure --cluster django-ecs-sample --default-launch-type EC2 --config-name django-ecs-sample --region ap-northeast-1

指定しているパラメータについて

  • --cluster : ここにはクラスター名を指定します。
  • --default-launch-type : ここには作成するインスタンスの種類を指定します。 EC2 を指定すると EC2インスタンスを立ち上げて docker 環境を構築します。 サーバレスの Fargate を指定することもできます。
  • --config-name : 作成する config の名前を指定します。
  • --region : EC2を立ち上げるリージョンを指定します(例 ap-northeast-1)

下記のように返ってくれば django-ecs-sample という名前のクラスター設定が保存され、使えるようになります。

INFO[0000] Saved ECS CLI cluster configuration django-ecs-sample.

(3) ECS クラスターを作成します

  • EC2 keypair を用意します。 superuser を作成する際に ssh で接続するため、ここで作成しておきます。 既存のキーペアーを持っていて使える場合はここを飛ばしても大丈夫です。 下記のコマンドを実行します。
shell
$ aws ec2 create-key-pair --key-name MyKeyPair --query 'KeyMaterial' --output text > MyKeyPair.pem

  'MyKeyPair' と 'MyKeyPair.pem' の部分にはわかりやすい名前を指定します。
  出来た pem ファイルを ~/.ssh/ フォルダへ保存しておきます。

shell
$ mkdir ~/.ssh
$ mv ./MyKeyPair.pem ~/.ssh

  また、作成した pem ファイルにはアクセス制限をかけておく事が推奨されていますので下記コマンドを実行します。(ubuntuの場合のみ)

shell
$ chmod 400 ~/.ssh/MyKeyPair.pem

詳細は下記の公式ページを参照して下さい。
Amazon EC2 キーペアの作成、表示、削除

  • ECS クラスターを作成します。 ここでは t2.micro のEC2インスタンスを1つ作ります。 (無料枠を使えない場合は t3.micro の方が安いのでそちらでも構いません。)
shell
$ ecs-cli up --keypair MyKeyPair --capability-iam --size 1 --instance-type t2.micro --cluster-config django-ecs-sample --ecs-profile default

  'MyKeyPair' には先ほど作成した keypair 名を指定します。
  "Cluster creation succeeded."` と表示されれば成功です。
  起動には数分間かかりますので気長に待ちましょう。
  またここでEC2インスタンスが立ち上がります。
  一応最初の1年間は無料枠となっているインスタンスを使っていますが、他にもインスタンスを立ち上げている等々条件によって無料にならない場合もあります。
  ECSクラスターの破棄を行うまでは課金されますので中断する場合などはご注意ください。
  下記のように表示されればクラスターの作成は成功です。

shell
VPC created: vpc-043b9c071191494f9
Security Group created: sg-0289045419da1393c
Subnet created: subnet-04f7f97d1b133656a
Subnet created: subnet-0d323c836901d8e40
Cluster creation succeeded.

  何らかの理由でクラスターの作成をやり直す場合は、一旦下記のコマンドでクラスターを削除してからやり直してください。

shell
$ ecs-cli down --force --cluster-config django-ecs-sample

6. 作成したクラスターにサービスを作成し、アプリケーションをデプロイします

下記コマンドでサービスを作成してデプロイします。

shell
$ ecs-cli compose --file docker-compose-ecs.yml service up --cluster-config django-ecs-sample

"ECS Service has reached a stable state" と表示されればデプロイ成功です。
以下のコマンドで実行しているタスクを確認できます。

shell
$ ecs-cli compose service ps
Name                                        State    Ports                    TaskDefinition     Health
26f9f2f5-7ff2-4f59-9852-1299c9572a59/nginx  RUNNING  3.113.1.25:80->80/tcp  django-ecs-sample:5  UNKNOWN
26f9f2f5-7ff2-4f59-9852-1299c9572a59/db     RUNNING                         django-ecs-sample:5  UNKNOWN
26f9f2f5-7ff2-4f59-9852-1299c9572a59/web    RUNNING                         django-ecs-sample:5  UNKNOWN

上記の場合 nginx の Ports に出力されている 3.113.1.25 がアクセスするIPアドレスとなります。
ブラウザを使ってアクセスするために控えておいて下さい。

7. Webブラウザで動作確認を行います

ChromeやFirefoxなどWebブラウザを起動し、上記で確認した nginx の Ports アドレスを指定します。

Chrome,Firefox
http://3.113.1.25/polls
※上記のURI 3.113.1.25 の部分はnginxのPortsアドレスに置き換えて下さい。

ブラウザ上に下記のように表示されれば成功です。

No polls are available.

もしも

OperationalError at /polls/
(2002, "Can't connect to MySQL server on 'db' (115)")

のようなエラーが出た場合は MySQL との接続がうまく行っていない可能性が高いです。
mydb.cnf の password と docker-compose-ecs.yml の MYSQL_ROOT_PASSWORD が同じか確認して下さい。
上記が違っていた場合は docker-compose-ecs.yml の MYSQL_ROOT_PASSWORD を mydb.cnf の password に合わせてください。
修正後、下記「後始末(サービスとリソースを削除する)」の1,2を実行してサービスとクラスターを削除した後、クラスターの作成からやり直してみて下さい。

8. ssh で AWS EC2 へログインし createsuperuser を行います

7 で No polls are available. と表示されれば MySQL との接続も問題なくアプリは完成!
と言いたい所ですが・・・
残念ながら superuser を作成していないため /admin でログインする事が出来ず Questions 等のデータを作成する事も出来ません。
どうしても createsuperuser を上手く実現する方法を思いつけず、苦し紛れの ssh 接続で対応する事にします。

手順

(1) EC2 のセキュリティグループのインバウンドで ssh を開放します

この設定はAWSコンソール上で行います。
Webブラウザを立ち上げてAWSアカウントへログインします。
EC2 コンソール画面を表示させ、ECS用に作成された EC2 インスタンスを選択します。
(もしくは ECS コンソール画面からインスタンスを選択して EC2 コンソールを出す事も出来ます。)
下記画像のインスタンスの詳細の右側赤枠にある「パブリック DNS (IPv4)」の値(ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com)を控えておきます。
続いて左下赤枠にあるセキュリティグループのリンクをクリックしてセキュリティグループ画面を表示します。

django-ecs-sample02.png

セキュリティグループの詳細で「インバウンド」タブを選択、「編集」ボタンをクリックします。

django-ecs-sample03.png

インバウンドルールの編集画面で、ルールの追加ボタンをクリックします。
タイプを「SSH」にしソースで「マイIP」を選択して保存をクリックします。
固定IPアドレスを使っている場合はIPアドレスが変わらないためこのままで問題ありませんが、それ以外の方は速やかに進めて下さい。

django-ecs-sample04.png

(2) ターミナル(もしくはGitBash)に戻り、ssh 接続を行います

下記のコマンドで ssh 接続を行います。
MyKeyPair.pem には クラスター作成時に作ったものを指定します。
また、ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.comの部分には上記 (1) で控えたパブリック DNS (IPv4)を入れます。
ec2-user というユーザー名は Amazon Linux 2 または Amazon Linux AMI を使った際に決まっているユーザー名です。
この辺りは SSH を使用した Linux インスタンスへの接続 を参照して下さい。

shell
$ ssh -i ~/.ssh/MyKeyPair.pem ec2-user@ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com

初めて ssh 接続する際は下記のような問いがでるので、yes と入力します。

shell
The authenticity of host 'ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com (XX.XX.XX.XX)' can't be established.
ECDSA key fingerprint is SHA256:XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.
Are you sure you want to continue connecting (yes/no)?

ログインに成功すると下記のようなプロンプト画面になります。

shell
[ec2-user@ip-10-0-1-38 ~]$

続いて起動している web コンテナのIDを調べます。

shell
$ docker ps
CONTAINER ID        IMAGE                                                                              COMMAND                  CREATED             STATUS                    PORTS                 NAMES
9b923b483350        XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-nginx:latest   "nginx -g 'daemon of…"   40 minutes ago      Up 40 minutes             0.0.0.0:80->80/tcp    ecs-django-ecs-sample-20-nginx-94c0f0a1f7c3e09a2200
8f06e3da926f        XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-web:latest     "sh /code/run-my-app…"   40 minutes ago      Up 40 minutes             8001/tcp              ecs-django-ecs-sample-20-web-e69bd295ee81ad978201
719a8da19a44        XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/django-ecs-sample-mysql:latest   "docker-entrypoint.s…"   41 minutes ago      Up 41 minutes             3306/tcp, 33060/tcp   ecs-django-ecs-sample-20-db-96eacce1d2fba3c8f501
862ad405bf2b        amazon/amazon-ecs-agent:latest                                                     "/agent"                 41 minutes ago      Up 41 minutes (healthy)                         ecs-agent  

上記の場合 django-ecs-sample-web:latest のタグが付いているコンテナIDは 8f06e3da926f なので、docker exec を使ってこのコンテナに入ります。

shell
$ docker exec -it 8f06e3da926f sh

/code $ とプロンプトが変わります。
これ以降ssh接続中の説明では $ 以前はプロンプトです。$ 以降をコマンドとして入力して下さい。
cd mysite でフォルダを /code/mysite/ へ移動します。念の為manege.pyがあるか ls コマンドで調べます。

shell
/code $ cd mysite
/code/mysite $ ls -l
total 20
-rwxr-xr-x    1 197612   197121         626 Jan 18 09:51 manage.py
-rwxr-xr-x    1 197612   197121         119 Jan 18 09:54 mydb.cnf
drwxr-xr-x    1 197612   197121        4096 Jan 18 09:56 mysite
drwxr-xr-x    1 197612   197121        4096 Jan 20 14:51 polls      

manage.py がありましたね。

それでは下記コマンドでスーパーユーザーを作成しましょう。

shell
/code/mysite $ python manage.py createsuperuser
ユーザー名 (leave blank to use '1000'): Admin
メールアドレス: Admin@test.com
Password:
Password (again):
Superuser created successfully.

上記のように、ユーザー名、EMailアドレス、パスワードの入力を求められるので適宜入力します。
(上記はサンプルです。)

shell
Superuser created successfully.

と出れば作成完了です。
docker コンテナと EC2 から exit でログアウトします。

shell
/code/mysite $ exit
[ec2-user@ip-10-0-1-38 ~]$ exit
ログアウト
Connection to ec2-XX-XX-XX-XX.ap-northeast-1.compute.amazonaws.com closed.

上記のように表示されれば ssh を使った作業は全て完了です。

(3) セキュリティグループに追加した ssh の設定を削除します

先程の AWS コンソールに戻りインバウンドルールの編集画面で下記の画像の赤枠✖をクリックし、保存します。
django-ecs-sample05.png

これで晴れて /admin でログインし、Questions と Choices を作成して Polls アプリを動かすことが出来ます。
お疲れ様でした!

9. 後始末(サービスとリソースを削除しておきましょう)

テストが終わったら無駄な課金をさけるためリソースを削除しておきます。
下記の手順で削除していきます。

(1) サービスを削除します

shell
$ ecs-cli compose --file docker-compose-ecs.yml service rm

(2) クラスターを削除します

shell
$ ecs-cli down --force
〜〜〜
  INFO[0121] Deleted cluster                               cluster=django-ecs-sample

という表示が出れば無事クラスターが削除されています。

(3) ECR イメージを削除します

ECR も課金されますので、使わなくなったら削除しておきましょう。(微々たる金額ですが)

shell
$ aws ecr batch-delete-image --repository-name django-ecs-sample-web --image-ids imageTag=latest
$ aws ecr batch-delete-image --repository-name django-ecs-sample-nginx --image-ids imageTag=latest
$ aws ecr batch-delete-image --repository-name django-ecs-sample-mysql --image-ids imageTag=latest

(4) ECR リポジトリを削除します

shell
$ aws ecr delete-repository --repository-name django-ecs-sample-web
$ aws ecr delete-repository --repository-name django-ecs-sample-nginx
$ aws ecr delete-repository --repository-name django-ecs-sample-mysql

ここまで削除すれば課金されることはありません。
但し、ecs-cli push を複数回行った場合など latest 以外のイメージがあるとリポジトリの削除に失敗する事があります。
そのような場合は AWS コンソールから ECR リソースを確認して削除してください。

(5) タスク定義を登録解除します

タスクも不要なのでリストアップされないようにしておきます。
まず下記コマンドでタスク定義のリストを表示させます。

shell
$ aws ecs list-task-definitions

下記のようにタスク定義が表示されます。

shell
{
    "taskDefinitionArns": [
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:1",
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:2",
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:3",
        "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:4"
    ]
}

今回いろいろテストしたためタスク定義が4つも作られていました(1度で成功した場合は1つしか作られません)。
これはタスク設定に変更があった場合 compose service up の度に新しいものが作成されるようです。
これら全てが不要ですので、下記のコマンドで登録解除します。

shell
$ aws ecs deregister-task-definition --task-definition django-ecs-sample:1

django-ecs-sample:1 の部分に上記リストのタスク名とリビジョンを入れます。
登録解除されれば解除されたタスク情報が返されます。
全てのリビジョンを登録解除して再び list-task-definitions で確認すると

shell
{
    "taskDefinitionArns": [
    ]
}

と、登録解除された事がわかります。
先程から登録解除と書いているようにあくまで解除されただけで削除されたわけではありません。
下記のコマンドで INACTIVE なタスク定義をリストアップさせると、

shell
$ aws ecs list-task-definitions --status INACTIVE
{
  "taskDefinitionArns": [
      "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:1",
      "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:2",
      "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:3",
      "arn:aws:ecs:ap-northeast-1:XXXXXXXXXXXX:task-definition/django-ecs-sample:4",
  ]
}

上記のように先程登録解除した django-ecs-sample:1 などが列挙されました。
これは既に動いているクラスターやサービスのタスク定義を登録解除してもタスクが終了するわけでは無いことを表しています。
またこれら登録解除済みのタスクを使っているクラスターのインスタンスが再起動した場合でも解除済みのタスク定義でタスクは起動します。
では何が違うのかと言えば、解除したタスク定義を使って新規でタスクを立ち上げる・サービスを更新する事は出来ないという意味です。
せめて何処でも使ってないタスク定義(今回のテストプログラムのようなもの)は削除したいと思いますが、今の所タスク定義を完全に削除する方法は無さそうです。

あとがき

最後までお付き合い頂きありがとうございました。
私がよく理解できてないため解りにくい所もあったかと思います。
また、デプロイ後にコードを修正したりアップデートする方法については書けていません、調べてやってみて頂ければと思います。

最初にも書きましたが、こちらは私が作ったDjangoのテストアプリを ECS へ出来るだけ簡単にデプロイする為に作ったサンプルを元にしています。
そのため私がいかに早く楽にデプロイできるかに重点を置きました。
本来重要なセキュリティなどは置き去りとも言えます・・・

なんにせよ docker と ECS を使えば簡単に Django アプリを AWS 上で動かせるんだなと思って頂けたらと思います。

最後になりましたが、これを書くにあたって沢山の記事、勉強会での発表等を参考にさせて頂きました。
それらに係わられた全ての方に感謝しています、ありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails+Redis+Docker】RailsアプリにRedis導入&ランキングの実装

Redisの導入

今回使うRedisはブログサイトでよくあるランキングを表示するために使います。

まずRailsとRedisの接続からしないといけないのですが、ここで2日くらい躓きました。色んなサイトを見まくって試したけど全然繋がらない状態が続き地獄でした。

解決方法

基本的にredisはlocalhost:6379に繋ぐのが普通なのですが、開発段階だとRails自体をlocalhostに繋いでるので混同しちゃって上手く繋がらなくなってしまっていたということでした。

なのでredisの設定をlocalhost→redisといった感じに名前を変更して行えばすんなり上手くいった感じです。

出来てしまえば簡単なことだったと思うのですが、やはり初心者には結構辛いところでした。コード打っててエラーならまだしも繋がらなくてエラーは精神的にかなりきます。

以下は変更点と参考サイトです。

https://stackoverflow.com/questions/34729752/sidekiq-error-connecting-to-redis-on-127-0-0-16379-errnoeconnrefused-on-doc】

ちなみにdocker-compose.ymlはこんな感じです。

docker-compose.yml
version: '3'

 services:
   db:
     image: mysql:8.0.17
     command: mysqld --default-authentication-plugin=mysql_native_password
     volumes:
       - ./db/mysql_data:/var/lib/mysql 
     environment:
       MYSQL_ROOT_PASSWORD: root
       MYSQL_DATABASE: root
     ports:
       - "4306:3306"

   web:
     build: .
     command: bundle exec rails s -p 3000 -b '0.0.0.0'
     volumes:
       - .:/app_name
     ports:
       - "3000:3000"
     depends_on:
       - db
       - redis
     tty: true
     stdin_open: true
     links:
       - db
     environment:
       REDIS_HOST: redis
       REDIS_PORT: 6379

   redis:
     image: redis:5.0.5
     ports:
       - 6379:6379
     volumes:
       - ./redis:/data
     command: redis-server --appendonly yes

Redisでランキング機能の実装

無事redisは導入出来ましたが、「導入出来てしまえばこっちのもん!」というわけではありません。

ランキングを表示しないといけないのでこれまたredisの基礎とcontroller、viewを見直さないといけません。

これも「Rails redis ランキング」と調べたら結構参考サイトは出てくるのですが、仕組みの理解が乏しいので基礎の見直しが必要でした。

redisの特徴としては以下のような感じです。

  • インメモリアルデータベース(すごく早い!)…ランキングなどに向いてる
  • 永続化(定期的にディスクに書き出す)
  • データ構造サーバー

そんなredisをRailsで使うには、gemでredisを導入してREDISメソッドを利用する必要があります。

gemの導入が完了したらconfig/initialize以下にredis.rbを作成して以下を記入。

config/initialize/redis.rb
require 'redis'

uri = URI.parse(ENV["REDIS"])
REDIS = Redis.new(host: uri.host, port: uri.port)
post_controller.rb
  def index
     @posts = Post.all
     ids = REDIS.zrevrangebyscore "posts/daily/#{Date.today.to_s}", "+inf", 0,limit:[0,3]
     @ranking_posts = ids.map{ |id| Post.find(id) }
   end

   # GET /posts/1
   # GET /posts/1.json
   def show
     REDIS.zincrby "posts/daily/#{Date.today.to_s}", 1, @post.id
   end 

今回はredisのソート済みセットを使ってpv数ランキングを実装していきました。
pv数の表示はviewに直接以下のように書けば表示されます。

index.rb
<ul>
   <% @ranking_posts.each do |ranking_post| %>
       <li>
         <%= link_to(ranking_post.title,"/posts/#{ranking_post.id}") %>
         (<%= REDIS.zscore("posts/daily/#{Date.today.to_s}", ranking_post.id).to_i %>PV)
       </li>
   <% end %>
</ul>

アクションに設置する方法がないか考えたのですが、これしか方法がわからなかったです。ちょっと見苦しいですがとりあえずこれでPV数が表示されます。

まとめ

Redisの理解はまだまだ乏しいのですが、触ってると結構楽しかったのでまた利用してみたいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Dockerfileに書いた字をBashに表示する(初心者備忘録)

Dockerfileを作る

ファイルの場所は適当でよし、とりあえずディレクトリ(ここではmy_build)を作り、その中にDockerfileを作る

Bash.shell
$ mkdir my_build
↓
$ cd my_build
↓
$ touch Dockerfile
↓

Dockerfileに書き込む

普通にファイルを開いて入力する

FROM ubuntu:18.04

CMD ["echo", "hello container"]

Dockerの内容に基づいてコンテナイメージを作成

上の続きから。
-tはコンテナイメージの名前をつけられます。
ここでいうところのwinterfall/hello

Bash.shell
↓
$ docker build -t winterfall/hello:1.0.0 .
↓

Dockerを実行

こうすることで、Dockerfileの["echo", "hello container"]の部分が実行されます。

Bash.shell
$ docker run winterfall/hello:1.0.0

>>> hello container

参考書

みんなのDocker/Kubernetes

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kaggle公式DockerイメージをGCPにデプロイしてGPU計算環境を準備する

ゴール

ローカルマシンからGCP上のVMインスタンスにsshして

$ gcloud compute ssh kaggle-gpu-compute -- -L 8080:localhost:8080

ブラウザからJupyterLabに接続できること

スクリーンショット 2020-02-09 19.04.56.png

更に、データセットをKaggleのノートブックと同じPATHで読み込めること。

スクリーンショット 2020-02-09 19.48.13.png

前提

  • GPUの割り当て申請が完了していること
  • gcloudのインストール、認証が完了していること
  • gcloudのデフォルトゾーンが設定されていること(筆者は以下の通りに設定しています)
$ gcloud config set compute/zone asia-northeast1-a

手順

VMインスタンスの作成、ログイン

以下のコマンドで一発で作成します。
OSはUbuntu 18.04、GPUはNVIDIA Tesla T4です。料金の節約のためpreemptibleで作成しています。
インスタンス名、machine-type等は適当なのでお好みに合わせて変更してして下さい。ストレージは、KaggleのDockerイメージが30GB近くあるので、コンペのデータをダウンロードすることなどを考えると100GB程度はあったほうが安心です。

$ gcloud compute instances create kaggle-gpu-compute \
    --image-family=ubuntu-1804-lts \
    --image-project=gce-uefi-images \
    --boot-disk-size="100" \
    --machine-type=n1-standard-2 \
    --accelerator type=nvidia-tesla-t4,count=1 \
    --maintenance-policy="TERMINATE" \
    --no-restart-on-failure \
    --preemptible

作成できたら、以下のコマンドでsshします。

$ gcloud compute ssh kaggle-gpu-compute

必要なパッケージのインストール

まっさらなUbuntuインスタンスなので、必要なパッケージをゴリゴリ入れていく必要があります。
まずGPUに対応したDockerコンテナを動かすために、以下が必要となります。

  • Docker
  • NVIDIA Container Toolkit

更に、Kaggleのデータセットをダウンロード、解凍するために以下が必要になります。

  • Kaggle API
  • unzip

これらをまとめてインストールするスクリプトを以下に用意しました。Ubuntu 18.04ならコピペで動くはずです。

set_up.sh
sudo apt -y install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
sudo apt update
sudo apt -y install docker-ce docker-ce-cli containerd.io
sudo curl -L https://github.com/docker/compose/releases/download//1.25.1-rc1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo add-apt-repository -y ppa:graphics-drivers/ppa
sudo apt update
sudo apt -y install ubuntu-drivers-common
sudo ubuntu-drivers autoinstall
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$(. /etc/os-release;echo $ID$VERSION_ID)/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list
sudo apt update
sudo apt -y install nvidia-container-toolkit
sudo reboot

これをインスタンス上に保存して実行します。

$ sh set_up.sh

最後まで完了するとrebootされて接続が切れるので、再起動が完了する頃合いをみてもう1度sshしてください。

Kaggleの公式イメージをbuild

GPU版のDockerイメージはまだ公開されていないそうなので、手動でビルドする必要があります。

https://github.com/Kaggle/docker-python

GPU: private for now, we will make it public soon.

といっても公式ドキュメントの手順通りに、リポジトリをcloneしてビルドするだけです(一時間半くらいかかりました)

$ git clone https://github.com/Kaggle/docker-python.git
$ cd docker-python
$ sudo ./build --gpu

完了すると以下の通りにkaggle/python-gpu-buildというイメージが出来上がっています。

$ sudo docker images
REPOSITORY                                   TAG
kaggle/python-gpu-build                      latest

kaggle/python-gpu-buildをベースにしたオリジナルのイメージを作成する

kaggle/python-gpu-buildをそのまま使用するのではなく、これをベースに新たなイメージを作成します。
まず、後々コンテナにマウントするためのディレクトリを作っておきます。

$ mkdir -p ~/kaggle/input/

ここで作成したinputディレクトリはKaggle APIを使ってダウンロードしたデータの置き場所です。
コンテナの外からここにデータを置いて、コンテナ内からアクセスできるようにします。

Dockerfileを書いて~/kaggle/配下に置きます。

FROM kaggle/python-gpu-build
ENV NVIDIA_VISIBLE_DEVICES all
ENV LD_LIBRARY_PATH=/usr/local/cuda/lib64
EXPOSE 8080
RUN mkdir -p /kaggle/working
WORKDIR /kaggle/working
CMD jupyter-lab --no-browser --port=8080 --ip=0.0.0.0 --allow-root --NotebookApp.token=''

設定する環境変数ですが、NVIDIA_VISIBLE_DEVICESはコンテナ内からどのGPUを認識できるかを指定する変数です。
LD_LIBRARY_PATHは以下の記事で解説されてる通り、CUDAのライブラリのPATHを通すために指定しています。
https://qiita.com/chrstphr_ml/items/c23b7ad2423c2c7b94f9#%E7%AB%8B%E3%81%A1%E4%B8%8A%E3%81%92
WORKDIRに/kaggle/workingを指定することによって、Kaggleのノートブックと同じディレクトリ構造で動作するようにしています。
CMDにはJuputerLabを起動するコマンドを指定しています。--NotebookApp.token=''をつけることにより、トークンなしで接続できます。

イメージをbuildしてコンテナを起動します。

$ cd ~/kaggle/
$ sudo docker build -t kaggle/jupyter .
$ sudo docker run -v `pwd`:/kaggle -p 8080:8080 -d --name kaggle --restart=always --gpus all kaggle/jupyter

--gpus allはNVIDIA Container Toolkitをインストールしたことにより使用可能なオプションです。GPUを使用するためには必須です。
--restart=alwaysを付けることにより、インスタンスが立ち上がると自動でコンテナが起動するようになります。

Kaggle APIが使えるようにする

kaggle.jsonをインスタンス~/.kaggle/に置く必要があるので適当なテキストエディタを使って配置します。

$ mkdir ~/.kaggle/
$ vim ~/.kaggle/kaggle.json # put your kaggle.json
$ chmod 600 ~/.kaggle/kaggle.json
$ kaggle --version
Kaggle API 1.5.6

試しにタイタニックコンペのデータセットをダウンロードしてzipを展開しておきましょう(タイタニックコンペに参加している前提)。

$ cd ~/kaggle/input/
$ kaggle competitions download -c titanic
$ unzip titanic.zip -d titanic

JupyterLabに接続する

sshポートフォワーディングを利用して、接続します。

$ gcloud compute ssh kaggle-gpu-compute -- -L 8080:localhost:8080

これでsshで接続している間は http://localhost:8080/ からJupyterLabに接続できるはずです。

動作確認

適当なノートブックを作成して、以下のコードを実行してください。

# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load in 

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)

# Input data files are available in the "../input/" directory.
# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory

import os
for dirname, _, filenames in os.walk('/kaggle/input'):
    for filename in filenames:
        print(os.path.join(dirname, filename))

# Any results you write to the current directory are saved as output.

先程ダウンロードしたデータセットが見れるはずです。

スクリーンショット 2020-02-09 19.48.13.png

また以下のコードを実行してTrueが返ってくればGPUも問題なく認識しています。

from tensorflow.python.client import device_lib
device_lib.list_local_devices()

nvidia-smiを実行してGPUの使用状況を確認することもできます。

!nvidia-smi
Sun Feb  9 11:07:41 2020       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 440.48.02    Driver Version: 440.48.02    CUDA Version: 10.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla T4            Off  | 00000000:00:04.0 Off |                    0 |
| N/A   45C    P0    25W /  70W |    222MiB / 15109MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
+-----------------------------------------------------------------------------+
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自分好みのdocker image を作る。(vue.cli編)

はじめに

自分用メモ
Dockerでimageを作成するには、以下の2通りがある。

  • Dockerfileから作る
  • 適当なOSのイメージをpullして、コンテナを作った後、bashにより自分好みのパッケージ等をインストールしてimage化する

今回は後者の方法でdocker imageを作成したのでそれをメモする。

好みのOSをえらんでコンテナ作成

今回はubuntuにしました。

$ docker run --name containerName -it ubuntu:18.04 /bin/bash

runコマンドでimageのpullとコンテナの作成をまとめてやってくれる。/bin/bashでシェルに接続。--name containerName でコンテナ名決めれる。

好みのパッケージ等を入れてく。

今回は

  • curl
  • yarn
  • node.js
  • npm
  • vue.cli
  • webpack

などを入れた。

image化

以下のコマンドを打ち込むだけ。

//まずはコンテナを止めましょう。
docker stop containerName
//そしてイメージ化
docker commit containerName imageName:version 

imageを起動

作ったイメージを起動してみよう。
以下のコマンドで入れる。今回はオプションとしてポート番号をつけている。

docker run --name containerName -p 1234:8080 -it imageName:version /bin/bash  

おわりに

分かれば簡単だけど、dockerのネットワークの仕組みとかが分からないとオプションとかの設定に悩みことになる。(なった)この記事は自分用なので他の方には分かり辛いと思う。そんな人には以下のyoutubeが分かりやすい。

https://www.youtube.com/watch?v=DS5HBTMG1RI&t=3s
https://www.youtube.com/watch?v=h6uw5c5GB_U

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「ERROR: The Compose file is invalid」となったクソしょうもない原因

その状況

docker-compose.yml
version: '3'

services:

  (略)

db:
    image: mysql:5.7
    container_name: db-host
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: database
      MYSQL_USER: docker
      MYSQL_PASSWORD: docker
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
    - ./docker/db/data:/var/lib/mysql
    - ./docker/db/my.cnf:/etc/mysql/conf.d/my.cnf
    - ./docker/db/sql:/docker-entrypoint-initdb.d
    ports:
    - 3306:3306

とdocker-compose.ymlを書き、docker-compose upしてみると

ERROR: The Compose file '.\docker-compose.yml' is invalid because:
Invalid top-level property "db". Valid top-level sections for this Compose file are: version, services, networks, volumes, and extensions starting with "x-".

You might be seeing this error because you're using the wrong Compose file version. Either specify a supported version (e.g "2.2" or "3.3") and place your service definitions under the `services` key, or omit the `version` key and place your service definitions at the root of the file to use version 1.
For more on the Compose file format versions, see https://docs.docker.com/compose/compose-file/

こんなエラーが…
「お前のファイルさ、書き方がなってないから無効な。公式のやつ見て来いよ」てなことを言われるのでした。

公式やほかの人たちのと比べてもいろいろ直したのだが通らず。小一時間格闘する羽目に。

判明した原因

半角スペースがないこと。

docker-compose.yml
version: '3'

services:

  (略)

db:
    image: mysql:5.7
    container_name: db-host

db:の前に半角スペースがなくservicesと同じ列にあることがわかります。
dbservicesとして定義するためこのままではエラーが出てしまうのでした。

docker-compose.yml
version: '3'

services:

  (略)

 db:
    image: mysql:5.7
    container_name: db-host

半角スペースを入れてdocker-compose upすると無事に通りました。
dockerが厳しいというより、ただのしょうもないミスでした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerでKea DHCPを構築。CiscoとArubaに同時にOption43が渡せるようになった。

はじめに

自前で運用しているISC-DHCPをDockerに乗せ換えました。
Kea-1.7系もありますが、stableのKea-1.6を導入しました。

いままで出来ていなかった、CiscoとArubaのAP混在環境で、それぞれの機器に適切にOption43が割り当てられるようにできました。
これで、マルチベンダー環境でしかも同一セグメントであっても、DHCPサーバひとつで、適切にコントローラ割り当てができるようになりました。

対象はIPv4のみです。そのうちIPv6もやろうと思います。

対象機器および環境

検証環境

  • CentOS8(8.1.1911)
  • Docker(19.03.5)
  • ISC-Kea(1.6) <-- DockerコンテナはCentOS7ベース
  • Cisco AP1700(AIR-CAP1702I-Q-K9)(15.3(3)JC15)
  • Cisco Virtual Wireless LAN Contoroller(8.2.170.0)
  • Aruba AP-105(6.4.4.12)
  • Aruba Mobility Controller 3200(6.4.4.12)

初期設定

設定ファイルが意外と長くて、Dockerfileに記載するのが面倒かつ、あまり意味がなさそうなので、イメージ作成時点では、パッケージのデフォルト設定のままです。
このエントリのとおりに設定をしていくと、以下のパラメータでリースされます。

項目
リース時間 10時間
DNSサーバ 10.254.10.241
suffix prosper2.net

リース対象セグメント

セグメント レンジ GW
10.1.20.0/24 10.1.20.33 - 10.1.20.62 10.1.20.1
10.1.22.0/24 10.1.22.33 - 10.1.20.230 10.1.22.1

Option43によりアクセスポイントに送付するコントローラアドレス

対象 VCI文字列 コントローラアドレス
Cisco Cisco AP 10.254.10.201,10.254.10.202
Aruba ArubaAP 10.254.10.206

CiscoはPrimary/Secondaryが渡せるので、2つ設定しました。
(Tertiaryも渡せるけど、あんま使わん)

作業内容

Dockerイメージの準備

再利用できるようにイメージをつくっておきます。ので、Dockerfileを作成します。
bridge経由になっているため、コンテナはホスト(172.17.0.1/32)からのみ受け付けとしています
KeaはCentOS8のパッケージがないので、CentOS8ホスト上にCentOS7のコンテナをつくっています。

dhcp.df
FROM centos:centos7
RUN yum -y install epel-release ; \
curl -1sLf \
  'https://dl.cloudsmith.io/public/isc/kea-1-6/cfg/setup/bash.rpm.sh' \
  | bash ; \
 yum -y update ; yum -y install isc-kea ; systemctl enable kea-dhcp4
CMD [ "/usr/sbin/init" ]

Dockerfileからイメージをビルド

Dockerfileが作成できたら、ビルドします。

# docker build --force-rm -t infraserv:dhcp . -f ./dhcp.df

もし、dnfでコケたら、ホスト機で以下のコマンドが効くかもしれない。

# firewall-cmd --add-masquerade --permanent
# firewall-cmd --reload

コンテナ生成

--privileges はなんか評判悪そう(?)なので、推奨されてるぽい方法でコンテナ生成します。

# docker run --cap-add sys_admin --security-opt seccomp:unconfined -it -d --name dhcp -v /sys/fs/cgroup:/sys/fs/cgroup:ro -p 67:67/udp infraserv:dhcp

firewallでポート許可

radiusサービスを許可

firewall-cmd --add-service=dhcp --zone=public --permanent
firewall-cmd --reload

実環境にあわせて調整

ご利用の環境にあわせて、設定ファイルを修正してください。
コンテナにログインして、ファイル修正していきます。

# docker exec -it dhcp /bin/bash

でログインして、以下ファイルを編集。
まるっとコピペでひとまず動きます。が、Docker内の文字コードと合わないので、コメントは削除してください。

/etc/kea/kea-dhcp4.conf
{
"Dhcp4": {

  # リース時間などの調整
  "renew-timer": 9000,
  "rebind-timer": 18000,
  "valid-lifetime": 36000,

  # IFの設定。Dockerのなかなので、eth0のままでOKと思います。
  "interfaces-config": {
      "interfaces": [ "eth0" ]
  },

  # デフォルトのまま
  "control-socket": {
    "socket-type": "unix",
    "socket-name": "/tmp/kea-dhcp4-ctrl.sock"
  },

  # ファイル名や形式がいろいろあるみたいですが、デフォルトのままでもOK。
  "lease-database": {
      "type": "memfile",
      "persist": true,
      "name": "/var/lib/kea/dhcp4.leases"
  },

  # アクセスポイントのOption43のための定義セクションです。
  # CiscoとArubaでコントローラが2種類なら、このままでOK
  "option-def": [
    {
      "name":  "Cisco_vWLC",
      "code":  241,
      "space": "vendor-encapsulated-options-space",
      "type":  "ipv4-address",
      "array": true
    },
    {
      "name":  "Aruba_MC",
      "code":  43,
      "type":  "ipv4-address"
    }
  ],

  # DHCPでとりに行く際の VCI(vendor-class-identifier) を参照して、コントローラを振り分けています。
  # もし、「古いAPは古いWLCに接続」などあれば、適宜 "test": "substring~~" の箇所を工夫してください。
  # CiscoのVCIは以下が参考になります。
  # https://www.cisco.com/c/ja_jp/support/docs/wireless-mobility/wireless-lan-wlan/97066-dhcp-option-43-00.html#anc5 
  "client-classes": [

    # Cisco APで始まるものは、10.254.10.201/202 に接続させにいきます。
    {
      "name": "CiscoAP",
      "test": "substring(option[60].hex,0,8) == 'Cisco AP'",
      "option-data": [
        {
          "name":       "Cisco_vWLC",
          "code":       241,
          "space":      "vendor-encapsulated-options-space",
          "csv-format": true,
          "data":       "10.254.10.201,10.254.10.202"
        },
        {
          "name": "vendor-encapsulated-options"
        }

      ]
    },

    # ArubaAPで始まるものは、10.254.10.201/202 に接続させにいきます。
    # ArubaAPのVCIは ArubaAP で共通のようです。以下参照
    # https://www.arubanetworks.com/techdocs/ArubaOS_80_Web_Help/Content/ArubaFrameStyles/DHCP_Option_43/Windows_Based_DHCP_Serve.htm
    {
      "name": "ArubaAP",
      "test": "substring(option[60].hex,0,7) == 'ArubaAP'",
      "option-data": [
        {
          "name":  "Aruba_MC",
          "code":  43,
          "data":  "10.254.10.206"
        },
        {
          "name":  "vendor-class-identifier",
          "data":  "ArubaAP"
        }
      ]
    }

  ],

  # DNS関連の設定
  "option-data": [
    {
      "name": "domain-name-servers",
      "data": "10.254.10.241"
    },
    {
      "name": "domain-name",
      "data": "prosper2.net"
    },
    {
      "name": "domain-search",
      "data": "prosper2.net"
    }
  ],

  # ここからサブネットの情報を記載していきます。
  "subnet4": [
    {
      "subnet": "10.1.20.0/24",
      "pools": [ { "pool": "10.1.20.33 - 10.1.20.62" } ],
      "option-data": [
        {
          "name": "routers",
          "data": "10.1.20.1"
        }
      ]
    },
    {
      "subnet": "10.1.22.0/24",
      "pools": [ { "pool": "10.1.22.33 - 10.1.22.230" } ],
      "option-data": [
        {
          "name": "routers",
          "data": "10.1.22.1"
        }
      ]
    }
  ],

  # ログはまだよく見ていませんが、とりあえずデフォルトでOKです。
  "loggers": [
    {
    "name": "kea-dhcp4",
    "output_options": [
      {
        "output": "stdout"
      }
    ],
    "severity": "INFO",
    "debuglevel": 0
    }
  ]
}

}

設定ファイルを修正したら、サービスを再起動
エラーがないか、いちおうみておく。

# systemctl restart kea-dhcp4
# systemctl status kea-dhcp4

Cisco機器の設定

enableになって、conf tしてから以下の設定をいれていきます。

!interfaceへhelperアドレスを追加
Vlan2020
  ip helper-address 10.254.10.251
Vlan2022
  ip helper-address 10.254.10.251
end

動作確認

CiscoAPのOption43

初期化ボタンを押しながら起動した際のログ
MACアドレス:5897.bd0b.bedc
IPアドレス:10.1.20.34
WLCアドレス:10.254.10.201/202
を取得して、コントローラVMwlc01にjoinしようとしてます。

( ~ 省略 ~ )
*Feb  9 03:13:55.035: Using SHA-2 signed certificate for image signing validation.
*Feb  9 03:13:55.327: %DHCP-6-ADDRESS_ASSIGN: Interface BVI1 assigned DHCP address 10.1.20.34, mask 255.255.255.0, hostname AP5897.bd0b.bedc
( ~ 省略 ~ )
Translating "CISCO-CAPWAP-CONTROLLER.prosper2.net"...domain server (10.254.10.241)
*Feb  9 03:14:29.659: %CAPWAP-5-DHCP_OPTION_43: Controller address 10.254.10.201 obtained through DHCP
*Feb  9 03:14:29.659: %CAPWAP-5-DHCP_OPTION_43: Controller address 10.254.10.202 obtained through DHCP
*Feb  9 03:14:39.707: AP has SHA2 MIC certificate - Using SHA2 MIC certificate for DTLS.
*Feb  9 07:38:46.000: %CAPWAP-5-DTLSREQSEND: DTLS connection request sent peer_ip: 10.254.10.201 peer_port: 5246
*Feb  9 07:38:46.263: %CAPWAP-5-DTLSREQSUCC: DTLS connection created sucessfully peer_ip: 10.254.10.201 peer_port: 5246
*Feb  9 07:38:46.263: %CAPWAP-5-SENDJOIN: sending Join Request to 10.254.10.201
*Feb  9 07:38:51.263: %CAPWAP-5-SENDJOIN: sending Join Request to 10.254.10.201
*Feb  9 07:38:52.035: %CAPWAP-5-JOINEDCONTROLLER: AP has joined controller VMwlc01
( ~ 省略 ~ )

無事にコントローラにjoinされました

Screenshot from Gyazo

Keaのログは同じMACアドレス、IPアドレスでリースされています。

10.1.20.34,58:97:bd:0b:be:dc,01:58:97:bd:0b:be:dc,36000,1581269861,1,1,1,ap5897.bd0b.bedc,0,

ArubaAPのOption43

apbootからfactory_reset→bootした際のログ

( ~ 省略 ~ )
DHCP IP address: 10.1.20.36
DHCP subnet mask: 255.255.255.0
DHCP def gateway: 10.1.20.1
DHCP DNS server: 10.254.10.241
DHCP DNS domain: prosper2.net
Controller address: 10.254.10.206
Using eth0 device
TFTP from server 10.254.10.206; our IP address is 10.1.20.36; sending through gateway 10.1.20.1
Filename 'mips32.ari'.
Load address: 0x2000000
Loading: #####T #####
( ~ 省略 ~ )
## Starting application at 0x80e00000 ...
Uncompressing..............................................
Aruba Networks
ArubaOS Version 6.4.4.12 (build 58581 / label #58581)
( ~ 省略 ~ )
10.1.20.36 255.255.255.0 10.1.20.1
Running ADP...Done. Master is 10.254.10.206
( ~ 省略 ~ )
bond0 address=d8:c7:c8:cf:2c:92
br0 address=d8:c7:c8:cf:2c:92
wifi0: AP type AP-105, radio 0, max_bssids 8
( ~ 省略 ~ )

無事にコントローラにjoinされました

Screenshot from Gyazo

Keaのログは同じMACアドレス、IPアドレスでリースされています。

10.1.20.36,d8:c7:c8:cf:2c:92,01:d8:c7:c8:cf:2c:92,36000,1581269931,1,0,0,,0,

さいごに

ArubaAPはVCIをAPに戻してやらないと、AP側でちゃんと応答せず、ここの調査に時間がかかりました。
Kea自体もまだまだメジャーでないので、情報が少なくて苦労しました。

出典

https://kb.isc.org/docs/isc-kea-packages
https://lists.isc.org/pipermail/kea-users/2016-January/000217.html
https://kea.readthedocs.io/en/kea-1.6.0/
https://cloudsmith.io/~isc/repos/kea-1-6/setup/#formats-rpm

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ルーティングは設定しているのにLaravelで404NotFound

1.現象

ルーティングの記述はlaravelの/routes/web.phpに確かに存在しているのに
404 Not Found nginx/1.17.8になってしまう。

スクリーンショット 2020-02-09 16.51.39.png

ルーティングはこちら

routes/web.php
Route::get('/index', 'HelloController@index');

php artisan route:list
でみてもあるんだよなあ

スクリーンショット 2020-02-09 16.57.25.png

2.バージョン

PHP7.2
laravel5.8
docker使用

3.解決方法

どうやらdefault.confのnginxの設定がうまく行ってなさそう

default.conf
server {
  listen 80;
    index index.php index.html;
    root /var/www/public;

  location / {
    root /var/www/public;
    index  index.html index.php;
    }

  location ~ \.php$ {

    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass php:9000;
    fastcgi_index index.php;
    include fastcgi_params;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      fastcgi_param PATH_INFO $fastcgi_path_info;
  }
}

試したところ、以下ではルートディレクトリ"/"しか表示できなかった。
この以下のlocation部分を変更してみる

  location / {
    root /var/www/public;
    index  index.html index.php;
    }

以下のように変更してファイルやディレクトリを探して、なければ、
/index.php$query_stringで内部リダイレクトさせてみる

  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

こうしました。

default.conf
server {
  listen 80;
    index index.php index.html;
    root /var/www/public;

  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

  location ~ \.php$ {

    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass php:9000;
    fastcgi_index index.php;
    include fastcgi_params;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      fastcgi_param PATH_INFO $fastcgi_path_info;
  }
}

変更後再度、docker-compose up -dを実行
表示できました〜

4.参考

他の方が詳しい記事を書いていました。
https://qiita.com/k_hoso/items/33ccb5e02e73a244ed31
https://teratail.com/questions/182086

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ルーティングはあるのにLaravelで404NotFound

1.現象

ルーティングの記述はlaravelの/routes/web.phpに確かに存在しているのに
404 Not Found nginx/1.17.8になってしまう。

スクリーンショット 2020-02-09 16.51.39.png

ルーティングはこちら

routes/web.php
Route::get('/index', 'HelloController@index');

php artisan route:list
でみてもあるんだよなあ

スクリーンショット 2020-02-09 16.57.25.png

2.バージョン

PHP7.2
laravel5.8
docker使用

3.解決方法

どうやらdefault.confのnginxの設定がうまく行ってなさそう

default.conf
server {
  listen 80;
    index index.php index.html;
    root /var/www/public;

  location / {
    root /var/www/public;
    index  index.html index.php;
    }

  location ~ \.php$ {

    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass php:9000;
    fastcgi_index index.php;
    include fastcgi_params;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      fastcgi_param PATH_INFO $fastcgi_path_info;
  }
}

試したところ、以下ではルートディレクトリ"/"しか表示できなかった。
この以下のlocation部分を変更してみる

  location / {
    root /var/www/public;
    index  index.html index.php;
    }

以下のように変更してファイルやディレクトリを探して、なければ、
/index.php$query_stringで内部リダイレクトさせてみる

  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

こうしました。

default.conf
server {
  listen 80;
    index index.php index.html;
    root /var/www/public;

  location / {
    try_files $uri $uri/ /index.php?$query_string;
  }

  location ~ \.php$ {

    try_files $uri =404;
    fastcgi_split_path_info ^(.+\.php)(/.+)$;
    fastcgi_pass php:9000;
    fastcgi_index index.php;
    include fastcgi_params;
      fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
      fastcgi_param PATH_INFO $fastcgi_path_info;
  }
}

変更後再度、docker-compose up -dを実行
表示できました〜

4.参考

他の方が詳しい記事を書いていました。
https://qiita.com/k_hoso/items/33ccb5e02e73a244ed31
https://teratail.com/questions/182086

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

コード書いたことないPdMやPOに捧ぐ、Rails on Dockerハンズオン vol.6 - Model validation -

この記事はなにか?
この記事はが社内のプログラミング未経験者、ビギナー向けに開催しているRuby on Rails on Dockerハンズオンの内容をまとめたものです。ていうかこの記事を基にそのままハンズオンします。ハンズオンは
1回の内容は喋りながらやると大体40~50分くらいになっています。お昼休みに有志でやっているからです。
現在進行形なので週1ペースで記事投稿していけるように頑張ります。
ビギナーの方のお役にたったり、同じように有志のハンズオンをしようとしている人の参考になれば幸いです。

他のハンズオンへのリンク
Vol.1 - Introduction -
Vol.2 - Hello, Rails on Docker -
Vol.3 - Scaffold, RESTful, MVC -
Vol.4 - Static pages -
Vol.5 - Model and CRUD -
・ Vol.6 - Model validation -

$, #, >について
$: ローカルでコマンドを実行するときは、頭に$をつけています。
#: コンテナの中でコマンドを実行するときは、頭に#をつけています。
>: Rails console内でコマンド(Rubyプログラム)を実行するときは、頭に>をつけています。

はじめに

第6回目となる今回は、前回作成したUserモデルにバリデーションを付けていきます。
バリデーションとは、日本語では『検証』と訳されますが、モデルの保存条件のようなもので、例えばnamenilじゃダメ、とかemail@が含まれていないとダメ、とかそういうやつです。

Validationをつけてみる

早速Validationを付けてみます。いよいよModelファイルをいじる時がきました。

app/models/user.rb
class User < ApplicationRecord
  validates :name,
    presence: true,
    length: { maximum: 50 }

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX },
    uniqueness: { case_sensitive: false }
end

Validationはvalidates [attribute_name], [validations]の形式で定義することができます。
一つの属性に対して複数のValidationを一気に定義していますね。
どんなValidationが定義されているのか紹介していきます!

presence

presenceは『存在性』のValidationです。presence: trueなので『存在しなければならない』ことを検証します。
ユーザー情報としてnameemailが不足しているのはおかしいですよね。
なのでnameemail両方にPresence validationを与えています。

動作を確認してみましょう。Rails consoleで確認していくのでまずはコンテナ&Rails consoleの起動から。

$ docker-compose up -d
$ docker-compose exec web ash
# rails c
> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

> user.save
   (0.3ms)  BEGIN
  User Exists? (6.7ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT $1  [["LIMIT", 1]]
   (0.5ms)  ROLLBACK
=> false

おっと、saveメソッドでfalseが返却されていますね。前回も少しお話しましたが、saveメソッドはデータのDB保存に失敗する場合(validationで引っかかる場合)、falseを返却するようになっています。

エラーの内容はuser.errorsに格納されます。中でもuser.errors.full_messagesを見れば、ユーザー向けのエラーメッセージが格納されているのでエラー理由が一目瞭然です。

> user.errors.full_messages
=> ["Nameを入力してください", "Emailを入力してください", "Emailは不正な値です"]

『を入力してください』がPresence validationに違反した場合のエラーメッセージです。

属性の日本語化

...
validationは日本語化されていますが属性は英語になっていますね...
それもそのはず。属性の表現の仕方を定義していないのですから!

ということで初期設定の時にi18n化対応したのと同じように、localesファイルを編集して『Name』を『お名前』、『Email』を『メールアドレス』と日本語化してあげましょう。

config/locales/ja.yml
ja:
  activerecord:
    attributes:
      user:
        name: "お名前"
        email: "メールアドレス"
    errors:
      ...

属性の名称はactiverecord.attributes.[model_name].[attribute_name]で定義します。
一度Rails consoleをリロードして、もう一度エラーを起こして確認してみましょう!

> reload!
Reloading...
=> true

> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

> user.save
   (0.3ms)  BEGIN
  User Exists? (3.0ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT $1  [["LIMIT", 1]]
   (1.6ms)  ROLLBACK
=> false

> user.errors.full_messages
=> ["お名前を入力してください", "メールアドレスを入力してください", "メールアドレスは不正な値です"]

属性も日本語化されたエラーメッセージに変わりました!

length

lengthは属性値の長さを検証するvalidationです。
maximumで最大文字数(最大桁数)、minimumで最小文字数(最小桁数)、inで最小と最大の範囲、isで特定の文字数(桁数)を検証します。
今回は、nameには最大50文字、emailには最大255文字のvalidationを定義しているので、エラーの確認をするために51文字のnameと256文字のemailを持つUserモデルをsaveしてみましょう。

> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

> user.name = "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

> user.email = "b" * 245 + "@sample.com"
=> "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb@sample.com"

> user.save
   (2.8ms)  BEGIN
  User Exists? (11.7ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb@sample.com"], ["LIMIT", 1]]
   (0.4ms)  ROLLBACK
=> false

> user.errors.full_messages
=> ["お名前は50文字以内で入力してください", "メールアドレスは255文字以内で入力してください"]

文字数について検証してくれていることが確認できました!

format

formatは正規表現とマッチするかを検証するvalidationです。
今回のケースでは、正規表現として/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/iを与えています。これはメールアドレスの正規表現として一般的なもので、簡単に言えば「【何か文字列】@【何か文字列】.【何か文字列】」を表しています。
正規表現の表現方法については今回は端折ります。例えば「Ruby 正規表現の使い方 - Qiita」などを参考に勉強してみてください。常に知っておく必要はあんまりないと思いますが、必要になったときに調べながらでも書けるようになっているのが望ましいでしょう。

では、早速このフォーマットバリデーションが正しく動作するかを確認します。例えば、「@」を抜いた「taro.com」なんていかがでしょうか?絶対メールアドレスじゃないのでちゃんとエラーになってほしいですよね。

> user = User.new(name: "taro", email: "taro.com")
=> #<User id: nil, name: "taro", email: "taro.com", created_at: nil, updated_at: nil>

> user.save
   (0.4ms)  BEGIN
  User Exists? (2.1ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "taro.com"], ["LIMIT", 1]]
   (0.3ms)  ROLLBACK
=> false

> user.errors.full_messages
=> ["メールアドレスは不正な値です"]

メールアドレスのフォーマットエラーになりました。

uniqueness

uniquenessは一意性を検証するvalidationです。
case_sensitivefalseに設定すると大文字小文字の区別をしないで一意性を検証するようになります。emailで「taro@sample.com」と「TARO@sample.com」は同じメールアドレスですのでこのオプションを付与してます。

では検証してみましょう。

> User.create(name: "taro", email: "taro@sample.com")
   (0.5ms)  BEGIN
  User Exists? (3.7ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "taro@sample.com"], ["LIMIT", 1]]
  User Create (2.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "taro"], ["email", "taro@sample.com"], ["created_at", "2020-01-30 13:58:08.104803"], ["updated_at", "2020-01-30 13:58:08.104803"]]
   (2.0ms)  COMMIT
=> #<User id: 2, name: "taro", email: "taro@sample.com", created_at: "2020-01-30 04:58:08", updated_at: "2020-01-30 04:58:08">

> user = User.create(name: "taro", email: "TARO@sample.com")
   (0.5ms)  BEGIN
  User Exists? (3.0ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "TARO@sample.com"], ["LIMIT", 1]]
   (1.1ms)  ROLLBACK
=> #<User id: nil, name: "taro", email: "TARO@sample.com", created_at: nil, updated_at: nil>

> user.errors.full_messages
=> ["メールアドレスはすでに存在します"]

emailの一意性チェックでエラーになったことがわかります。また、大文字小文字を区別せずに一意性チェックをしてくれていることもわかりました。

DBの制約

ここまでモデル側、つまりアプリ側にvalidationをかけてきました。
このような制約はDB側でもかけることができますし、その方が安全だ!という考え方もあります。

Railsでは、デザインパターンとしてActiveRecordを採用しています。ActiveRecordでは「制約をかけるのはモデルの仕事」とされているため、上のようにモデル側で制約をかけています。
こうすることで、制約の内容が変わったとしてもDB側の設定を変えることなくアプリ側だけの改修で柔軟に対応をすることができます。

一方で一意性に関しては、複数のアプリが並列で処理を行う構成を考えるとDB側で制御されている方がよいとされています。

今回は一意性(uniqueness)に関して、DB側にも制約をかけます。

まず、マイグレーションファイルを生成します。

# rails g migration add_index_to_user
Running via Spring preloader in process 194
      invoke  active_record
      create    db/migrate/YYYYMMDDhhmmss_add_index_to_user.rb

これでほとんど空のマイグレーションファイルが生成されますので、中身を書いていきます。

db/migrate/YYYYMMDDhhmmss_add_index_to_user.rb
class AddIndexToUser < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, unique: true
  end
end

uniquenessの制約はadd_index [table (model)], [column (attribute)], unique: trueで設定します。
ではマイグレーションファイルを適用していきましょー。

# rails db:migrate
== 20200130053807 AddIndexToUser: migrating ===================================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0942s
== 20200130053807 AddIndexToUser: migrated (0.0947s) ==========================

DBの一意性チェックは利用するDBによりますが大文字小文字を区別してしまう可能性があります。そのため、モデル側でDBに保存する前に強制的にemailを小文字化する処理を入れます。

app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  ...
end

user.saveをするときに、RailsではCallbacksといいますがシーケンシャルな処理が存在します。まずvalidationが実行されsaveが実行されcommitが実行されるといった流れです。
before_saveはその名前から分かるとおり、validationが通った後、saveが始まる前に処理を挟み込むことを意味しています。before_saveの後の{}の中身が処理になりますが、email.downscaleで現在のemailを小文字化して再度self.email、つまり自分の属性値に代入しています。

さて、ここまでやるとDBでも一意制約を入れることができている状態になります。

一度モデル側のuniqueness validationを外しておき、DBだけで一意制約を担保できるか確認してみましょう。

app/models/user.rb
class User < ApplicationRecord
  ...
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX }#,
    # uniqueness: { case_sensitive: false }
end
> reload!
Reloading...
=> true

> user = User.create(name: "taro", email: "TARO@sample.com")
   (0.5ms)  BEGIN
  User Create (13.3ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "taro"], ["email", "taro@sample.com"], ["created_at", "2020-01-30 16:12:37.414147"], ["updated_at", "2020-01-30 16:12:37.414147"]]
   (0.6ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):40
ActiveRecord::RecordNotUnique (PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "index_users_on_email")
DETAIL:  Key (email)=(taro@sample.com) already exists.

> user.errors.full_messages
=> []

今回もROLLBACKが走っているので失敗していることがわかります。
違う点としては、ROLLBACKの理由がモデルのvalidationではなくActiveRecord::RecordNotUnique例外であるということがコンソールから読み取れると思います。モデルでのエラーではないので、user.errors.full_messagesには何も格納されていません。

さて、DB側の制約でも一意性を担保できることを確認できたので、モデルのコメントアウトを元に戻しておきます。

app/models/user.rb
class User < ApplicationRecord
  ...
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX },
    uniqueness: { case_sensitive: false }
end

後片付け

じゃあ、またデータ消します。

$ docker-compose down
$ docker-compose run --rm web rails db:migrate:reset

まとめ

今回は、Modelにvalidationを付与してみました。

ふんふん。ちゃんとvalidationをつけれましたね。
validationは他にもいろいろあります。
Active Record バリデーション - Railsガイド
自分のアプリケーションに合致するバリデーションを見つけましょう!

次回は、Userモデルにセキュアなパスワードを付与して行こうと思います。
パスワードを平文で持つのはやっぱりNG。Railsは簡単にセキュアなパスワードを実装できるんです!

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

Reference

P.S. 間違っているところ、抜けているところ、説明の仕方を変えるとよりわかりやすくなるところなどありましたら、優しくアドバイスいただけると助かります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SEしてるけど実はあんまりコード書いたことないんだよねって人に捧ぐ、Rails on Dockerハンズオン vol.6 - Model validation -

はじめに

第6回目となる今回は、前回作成したUserモデルにバリデーションを付けていきます。
バリデーションとは、日本語では『検証』と訳されますが、モデルの保存条件のようなもので、例えばnamenilじゃダメ、とかemail@が含まれていないとダメ、とかそういうやつです。

Validationをつけてみる

早速Validationを付けてみます。いよいよModelファイルをいじる時がきました。

app/models/user.rb
class User < ApplicationRecord
  validates :name,
    presence: true,
    length: { maximum: 50 }

  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX },
    uniqueness: { case_sensitive: false }
end

Validationはvalidates [attribute_name], [validations]の形式で定義することができます。
一つの属性に対して複数のValidationを一気に定義していますね。
どんなValidationが定義されているのか紹介していきます!

presence

presenceは『存在性』のValidationです。presence: trueなので『存在しなければならない』ことを検証します。
ユーザー情報としてnameemailが不足しているのはおかしいですよね。
なのでnameemail両方にPresence validationを与えています。

動作を確認してみましょう。Rails consoleで確認していくのでまずはコンテナ&Rails consoleの起動から。

$ docker-compose up -d
$ docker-compose exec web ash
# rails c
> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

> user.save
   (0.3ms)  BEGIN
  User Exists? (6.7ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT $1  [["LIMIT", 1]]
   (0.5ms)  ROLLBACK
=> false

おっと、saveメソッドでfalseが返却されていますね。前回も少しお話しましたが、saveメソッドはデータのDB保存に失敗する場合(validationで引っかかる場合)、falseを返却するようになっています。

エラーの内容はuser.errorsに格納されます。中でもuser.errors.full_messagesを見れば、ユーザー向けのエラーメッセージが格納されているのでエラー理由が一目瞭然です。

> user.errors.full_messages
=> ["Nameを入力してください", "Emailを入力してください", "Emailは不正な値です"]

『を入力してください』がPresence validationに違反した場合のエラーメッセージです。

属性の日本語化

...
validationは日本語化されていますが属性は英語になっていますね...
それもそのはず。属性の表現の仕方を定義していないのですから!

ということで初期設定の時にi18n化対応したのと同じように、localesファイルを編集して『Name』を『お名前』、『Email』を『メールアドレス』と日本語化してあげましょう。

config/locales/ja.yml
ja:
  activerecord:
    attributes:
      user:
        name: "お名前"
        email: "メールアドレス"
    errors:
      ...

属性の名称はactiverecord.attributes.[model_name].[attribute_name]で定義します。
一度Rails consoleをリロードして、もう一度エラーを起こして確認してみましょう!

> reload!
Reloading...
=> true

> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

> user.save
   (0.3ms)  BEGIN
  User Exists? (3.0ms)  SELECT 1 AS one FROM "users" WHERE "users"."email" IS NULL LIMIT $1  [["LIMIT", 1]]
   (1.6ms)  ROLLBACK
=> false

> user.errors.full_messages
=> ["お名前を入力してください", "メールアドレスを入力してください", "メールアドレスは不正な値です"]

属性も日本語化されたエラーメッセージに変わりました!

length

lengthは属性値の長さを検証するvalidationです。
maximumで最大文字数(最大桁数)、minimumで最小文字数(最小桁数)、inで最小と最大の範囲、isで特定の文字数(桁数)を検証します。
今回は、nameには最大50文字、emailには最大255文字のvalidationを定義しているので、エラーの確認をするために51文字のnameと256文字のemailを持つUserモデルをsaveしてみましょう。

> user = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>

> user.name = "a" * 51
=> "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"

> user.email = "b" * 245 + "@sample.com"
=> "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb@sample.com"

> user.save
   (2.8ms)  BEGIN
  User Exists? (11.7ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb@sample.com"], ["LIMIT", 1]]
   (0.4ms)  ROLLBACK
=> false

> user.errors.full_messages
=> ["お名前は50文字以内で入力してください", "メールアドレスは255文字以内で入力してください"]

文字数について検証してくれていることが確認できました!

format

formatは正規表現とマッチするかを検証するvalidationです。
今回のケースでは、正規表現として/\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/iを与えています。これはメールアドレスの正規表現として一般的なもので、簡単に言えば「【何か文字列】@【何か文字列】.【何か文字列】」を表しています。
正規表現の表現方法については今回は端折ります。例えば「Ruby 正規表現の使い方 - Qiita」などを参考に勉強してみてください。常に知っておく必要はあんまりないと思いますが、必要になったときに調べながらでも書けるようになっているのが望ましいでしょう。

では、早速このフォーマットバリデーションが正しく動作するかを確認します。例えば、「@」を抜いた「taro.com」なんていかがでしょうか?絶対メールアドレスじゃないのでちゃんとエラーになってほしいですよね。

> user = User.new(name: "taro", email: "taro.com")
=> #<User id: nil, name: "taro", email: "taro.com", created_at: nil, updated_at: nil>

> user.save
   (0.4ms)  BEGIN
  User Exists? (2.1ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "taro.com"], ["LIMIT", 1]]
   (0.3ms)  ROLLBACK
=> false

> user.errors.full_messages
=> ["メールアドレスは不正な値です"]

メールアドレスのフォーマットエラーになりました。

uniqueness

uniquenessは一意性を検証するvalidationです。
case_sensitivefalseに設定すると大文字小文字の区別をしないで一意性を検証するようになります。emailで「taro@sample.com」と「TARO@sample.com」は同じメールアドレスですのでこのオプションを付与してます。

では検証してみましょう。

> User.create(name: "taro", email: "taro@sample.com")
   (0.5ms)  BEGIN
  User Exists? (3.7ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "taro@sample.com"], ["LIMIT", 1]]
  User Create (2.5ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "taro"], ["email", "taro@sample.com"], ["created_at", "2020-01-30 13:58:08.104803"], ["updated_at", "2020-01-30 13:58:08.104803"]]
   (2.0ms)  COMMIT
=> #<User id: 2, name: "taro", email: "taro@sample.com", created_at: "2020-01-30 04:58:08", updated_at: "2020-01-30 04:58:08">

> user = User.create(name: "taro", email: "TARO@sample.com")
   (0.5ms)  BEGIN
  User Exists? (3.0ms)  SELECT 1 AS one FROM "users" WHERE LOWER("users"."email") = LOWER($1) LIMIT $2  [["email", "TARO@sample.com"], ["LIMIT", 1]]
   (1.1ms)  ROLLBACK
=> #<User id: nil, name: "taro", email: "TARO@sample.com", created_at: nil, updated_at: nil>

> user.errors.full_messages
=> ["メールアドレスはすでに存在します"]

emailの一意性チェックでエラーになったことがわかります。また、大文字小文字を区別せずに一意性チェックをしてくれていることもわかりました。

DBの制約

ここまでモデル側、つまりアプリ側にvalidationをかけてきました。
このような制約はDB側でもかけることができますし、その方が安全だ!という考え方もあります。

Railsでは、デザインパターンとしてActiveRecordを採用しています。ActiveRecordでは「制約をかけるのはモデルの仕事」とされているため、上のようにモデル側で制約をかけています。
こうすることで、制約の内容が変わったとしてもDB側の設定を変えることなくアプリ側だけの改修で柔軟に対応をすることができます。

一方で一意性に関しては、複数のアプリが並列で処理を行う構成を考えるとDB側で制御されている方がよいとされています。

今回は一意性(uniqueness)に関して、DB側にも制約をかけます。

まず、マイグレーションファイルを生成します。

# rails g migration add_index_to_user
Running via Spring preloader in process 194
      invoke  active_record
      create    db/migrate/YYYYMMDDhhmmss_add_index_to_user.rb

これでほとんど空のマイグレーションファイルが生成されますので、中身を書いていきます。

db/migrate/YYYYMMDDhhmmss_add_index_to_user.rb
class AddIndexToUser < ActiveRecord::Migration[6.0]
  def change
    add_index :users, :email, unique: true
  end
end

uniquenessの制約はadd_index [table (model)], [column (attribute)], unique: trueで設定します。
ではマイグレーションファイルを適用していきましょー。

# rails db:migrate
== 20200130053807 AddIndexToUser: migrating ===================================
-- add_index(:users, :email, {:unique=>true})
   -> 0.0942s
== 20200130053807 AddIndexToUser: migrated (0.0947s) ==========================

DBの一意性チェックは利用するDBによりますが大文字小文字を区別してしまう可能性があります。そのため、モデル側でDBに保存する前に強制的にemailを小文字化する処理を入れます。

app/models/user.rb
class User < ApplicationRecord
  before_save { self.email = email.downcase }
  ...
end

user.saveをするときに、RailsではCallbacksといいますがシーケンシャルな処理が存在します。まずvalidationが実行されsaveが実行されcommitが実行されるといった流れです。
before_saveはその名前から分かるとおり、validationが通った後、saveが始まる前に処理を挟み込むことを意味しています。before_saveの後の{}の中身が処理になりますが、email.downscaleで現在のemailを小文字化して再度self.email、つまり自分の属性値に代入しています。

さて、ここまでやるとDBでも一意制約を入れることができている状態になります。

一度モデル側のuniqueness validationを外しておき、DBだけで一意制約を担保できるか確認してみましょう。

app/models/user.rb
class User < ApplicationRecord
  ...
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX }#,
    # uniqueness: { case_sensitive: false }
end
> reload!
Reloading...
=> true

> user = User.create(name: "taro", email: "TARO@sample.com")
   (0.5ms)  BEGIN
  User Create (13.3ms)  INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"  [["name", "taro"], ["email", "taro@sample.com"], ["created_at", "2020-01-30 16:12:37.414147"], ["updated_at", "2020-01-30 16:12:37.414147"]]
   (0.6ms)  ROLLBACK
Traceback (most recent call last):
        1: from (irb):40
ActiveRecord::RecordNotUnique (PG::UniqueViolation: ERROR:  duplicate key value violates unique constraint "index_users_on_email")
DETAIL:  Key (email)=(taro@sample.com) already exists.

> user.errors.full_messages
=> []

今回もROLLBACKが走っているので失敗していることがわかります。
違う点としては、ROLLBACKの理由がモデルのvalidationではなくActiveRecord::RecordNotUnique例外であるということがコンソールから読み取れると思います。モデルでのエラーではないので、user.errors.full_messagesには何も格納されていません。

さて、DB側の制約でも一意性を担保できることを確認できたので、モデルのコメントアウトを元に戻しておきます。

app/models/user.rb
class User < ApplicationRecord
  ...
  validates :email,
    presence: true,
    length: { maximum: 255 },
    format: { with: VALID_EMAIL_REGEX },
    uniqueness: { case_sensitive: false }
end

後片付け

じゃあ、またデータ消します。

$ docker-compose down
$ docker-compose run --rm web rails db:migrate:reset

まとめ

今回は、Modelにvalidationを付与してみました。

ふんふん。ちゃんとvalidationをつけれましたね。
validationは他にもいろいろあります。
Active Record バリデーション - Railsガイド
自分のアプリケーションに合致するバリデーションを見つけましょう!

次回は、Userモデルにセキュアなパスワードを付与して行こうと思います。
パスワードを平文で持つのはやっぱりNG。Railsは簡単にセキュアなパスワードを実装できるんです!

では、次回も乞うご期待!ここまでお読みいただきありがとうございました!

Reference

Links

Vol.1 - Introduction -
Vol.2 - Hello, Rails on Docker -
Vol.3 - Scaffold, RESTful, MVC -
Vol.4 - Static pages -
Vol.5 - Model and CRUD -
・ Vol.6 - Model validation - ?この記事

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker for Windows and Mac

WindowsとMacそれぞれにDockerを導入する手順を記述する.(2020年2月9日現在)
いろんな方の記事を参考にさせていただいています.ありがとうございます.

Windows

はじめに現在使っているのがHomeなのかProなのかを確認する.

cmd
> systeminfo

「OS 名: 」以降を確認します。

Homeの場合

Proの場合

yper-V(仮想化システム)がサポートされているため
Docker for Windowsを使用します。

Hyper-Vの有効化

コントロールパネルの[プログラム]-[Windowsの機能の有効化または無効化]から「Hyper-V」にチェックを付けます。
※有効化するにはOSの再起動が必要です。

Docker for Windowsのインストール

以下のサイトから.exeファイルをインストール
https://docs.docker.com/docker-for-windows/install/
安定板のStable channelを選びます。

「Add shortcut to desktop」にチェックを付けて「OK」をクリックします。
インストール終了後、ダイアログの指示通りLog outして
「OK」を選択することでOSの再起動が行われます。

参考

Mac

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

proxyサーバ使用環境で、node-red(docker起動)の処理ノードを追加する方法

1. 経緯

node-redはdockerのコンテナが提供されているので、気軽に始めることが出来ます。
Dockerで実行する!

ハマった点として、proxy環境内でnode-redを起動して処理ノードを追加するとエラーが発生して追加できませんでした。
調べてみると、処理ノードを追加時にnpmコマンドが動作してノード追加を行っているらしく、npmコマンドにproxy設定を追加してやる必要があるようでした。
下記に、その手順を記載します。

2. 手順

  • node-redを起動する
$ docker run -it -d -p 1880:1880 --name mynodered nodered/node-red
  • rootユーザでnode-redeのコンテナにログインする
    node-redユーザでログインすると、sudoコマンドが入っていないため、npmへproxy設定ができないようです。
    そのため、-u rootでログインする必要があります。
$ guest@guest-VirtualBox:~$ docker exec -it -u root <起動中のnode-redのコンテナID> /bin/bash

  • npmコマンドのproxy設定を追加
    rootでコンテナにログインできるので、http,httpsのproxy と registryを設定する
$ npm -g config set proxy プロキシサーバのURL:port番号
$ npm -g config set https-proxy プロキシサーバのURL:port番号
$ npm -g config set registry http://registry.npmjs.org/
$ npm config list
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Herokuではレイヤー数が多過ぎるとDockerイメージが動かない

Herokuの Container Registry でDockerイメージが動かなくてハマったので後世のために記しておきます。

結論から言うと、Dockerイメージのレイヤーー数が40を超えるとHeroku上では動かせません(2020年2月9日時点)。レイヤー数40の制限の詳細な仕様、実装がどうなっているのかは分かりませんが、40近い命令数になると起こりうると注意した方がよいかと。

Images with more than 40 layers may fail to start in the Common Runtime

https://devcenter.heroku.com/articles/container-registry-and-runtime#known-issues-and-limitations

レイヤー数はDockerファイルに記載されている命令数に依存するのでとにかく命令数を減らすしかありません。
特に RUN 命令はまとめましょう。
なお、Dockerファイルに記載していた命令数は39でしたが、この制限に引っ掛かていたようです。
最終的には31まで命令数を減らして対応しました。

Dockerイメージが動かない時の症例

heroku run bash

heroku run bash で接続を試みると以下の状態のまま動かなくなる。

$ heroku run bash --type=worker --app [APP_NAME]
Running bash on ⬢ [APP_NAME]... connecting, worker.7390 (Standard-1X)

Heroku Scheduler

Heroku Scheduler でイメージ上のアプリケーションを実行しようとすると、以下のログが出るだけで実行されない。

Jan 28 13:12:00 [APP_NAME] app/api: Starting process with command [COMMAND] by user scheduler@addons.heroku.com
Jan 28 13:12:26 [APP_NAME] heroku/scheduler.8466: State changed from starting to complete
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ShinyProxyをDockerで動かす

ShinyProxyをDockerで動かす

ネットに転がっているexampleそのままです。
備忘録として残しておきます。

Dockerfileを作成

Dockerfile
FROM openjdk:8-jre

RUN mkdir -p /opt/shinyproxy/
RUN wget https://www.shinyproxy.io/downloads/shinyproxy-2.3.0.jar -O /opt/shinyproxy/shinyproxy.jar
COPY application.yml /opt/shinyproxy/application.yml

WORKDIR /opt/shinyproxy/
CMD ["java", "-jar", "/opt/shinyproxy/shinyproxy.jar"]

application.ymlを作成

application.yml
proxy:
  port: 8080
  authentication: simple
  admin-groups: admins
  users:
  - name: jack
    password: password
    groups: admins
  - name: jeff
    password: password
  docker:
      internal-networking: true
  specs:
  - id: 01_hello
    display-name: Hello Application
    description: Application which demonstrates the basics of a Shiny app
    container-cmd: ["R", "-e", "shinyproxy::run_01_hello()"]
    container-image: openanalytics/shinyproxy-demo
    container-network: sp-example-net
  - id: 06_tabsets
    container-cmd: ["R", "-e", "shinyproxy::run_06_tabsets()"]
    container-image: openanalytics/shinyproxy-demo
    container-network: sp-example-net

logging:
  file:
    shinyproxy.log

サンプルアプリのimageをpull

$ sudo docker pull openanalytics/shinyproxy-demo

networkを作成

network名はsp-example-net

sudo docker network create sp-example-net

imageをbuild

image名はshinyproxy-example

sudo docker build . -t shinyproxy-example

containerを起動

sudo docker run -d -v /var/run/docker.sock:/var/run/docker.sock --net sp-example-net -p 8080:8080 shinyproxy-example

確認

http://localhost:8080

  • username:jack
  • password:password

と打って、以下みたいなのが出てくればOKです。
Screen Shot 2020-02-09 at 2.04.55.png

参考

ほぼ(というか完全に)以下の通りです。
https://github.com/openanalytics/shinyproxy-config-examples/tree/master/02-containerized-docker-engine

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ShinyProxyをDocker containerで動かす

ネットに転がっているexampleそのままです。
備忘録として残しておきます。

Dockerfileを作成

Dockerfile
FROM openjdk:8-jre

RUN mkdir -p /opt/shinyproxy/
RUN wget https://www.shinyproxy.io/downloads/shinyproxy-2.3.0.jar -O /opt/shinyproxy/shinyproxy.jar
COPY application.yml /opt/shinyproxy/application.yml

WORKDIR /opt/shinyproxy/
CMD ["java", "-jar", "/opt/shinyproxy/shinyproxy.jar"]

application.ymlを作成

application.yml
proxy:
  port: 8080
  authentication: simple
  admin-groups: admins
  users:
  - name: jack
    password: password
    groups: admins
  - name: jeff
    password: password
  docker:
      internal-networking: true
  specs:
  - id: 01_hello
    display-name: Hello Application
    description: Application which demonstrates the basics of a Shiny app
    container-cmd: ["R", "-e", "shinyproxy::run_01_hello()"]
    container-image: openanalytics/shinyproxy-demo
    container-network: sp-example-net
  - id: 06_tabsets
    container-cmd: ["R", "-e", "shinyproxy::run_06_tabsets()"]
    container-image: openanalytics/shinyproxy-demo
    container-network: sp-example-net

logging:
  file:
    shinyproxy.log

サンプルアプリのimageをpull

$ sudo docker pull openanalytics/shinyproxy-demo

networkを作成

network名はsp-example-net

sudo docker network create sp-example-net

imageをbuild

image名はshinyproxy-example

sudo docker build . -t shinyproxy-example

containerを起動

sudo docker run -d -v /var/run/docker.sock:/var/run/docker.sock --net sp-example-net -p 8080:8080 shinyproxy-example

確認

http://localhost:8080

  • username:jack
  • password:password

と打って、以下みたいなのが出てくればOKです。
Screen Shot 2020-02-09 at 2.04.55.png

参考

ほぼ(というか完全に)以下の通りです。
https://github.com/openanalytics/shinyproxy-config-examples/tree/master/02-containerized-docker-engine

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DockerのFreeRADIUSでCiscoのログイン認証+MAC認証+ダイナミックVLANした(CentOS8)

はじめに

自前で運用しているFreeRADIUSをDockerに乗せ換えるべく、Dockerに初チャレンジです。
なお、本文で「MAC認証」の記載揺れがありますが、すべて「MACアドレスバイパス(MacAddressBypass)」の意味で用いています。
(MACアドレスを利用して「認証」するわけではない、という理解です)

対象機器および環境

検証環境

  • CentOS8(8.1.1911)
  • Docker(19.03.5)
  • FreeRADIUS(3.0.17)
  • Cisco841M(15.5(3)M5)

初期状態で認証可能なもの

初期状態では以下の2ユーザとenableパスワードが利用できるようになります。

ユーザ名 パスワード 備考
foo bar 通常ユーザでのログイン
hoge fuga ログインすると自動で特権に昇格
\$enab15\$ fuga enableコマンドで遷移する際のパスワード

MACアドレス+ダイナミックVLANは以下のもので登録された状態です。

MACアドレス VLAN文字列
055dc061bf92 default_seg

作業内容

Dockerイメージの準備

再利用できるようにイメージをつくっておきます。
Dockerfileを作成します。
bridge経由になっているため、コンテナはホスト(172.17.0.1/32)からのみ受け付けとしています

radius.df
FROM centos:centos8
RUN dnf -y update ; dnf install -y freeradius ; \
sed -i -e "s/auth = no/auth = yes/" /etc/raddb/radiusd.conf ;  \
echo $'\n\
client radius_clients { \n\
    ipaddr  = 172.17.0.1\n\
    netmask = 32\n\
    secret  = RADIUS_SECRET\n\
}\n\
' > /etc/raddb/clients.conf ; \
echo $'\n\
$INCLUDE /etc/raddb/mods-config/files/userlist/users.login\n\
$INCLUDE /etc/raddb/mods-config/files/userlist/mabs\n\
' > /etc/raddb/mods-config/files/authorize ; \
mkdir /etc/raddb/mods-config/files/userlist ; \
echo $'\n\
foo       Auth-Type := PAP , Cleartext-Password := "bar"\n\
$enab15$  Auth-Type := PAP , MD5-Password := "c32ec965db3295bad074d2afa907b1c3"\n\
hoge      Auth-Type := PAP , MD5-Password := "c32ec965db3295bad074d2afa907b1c3"\n\
          Service-Type := NAS-Prompt-User , Cisco-AVPair := "shell:priv-lvl=15"\n\
' > /etc/raddb/mods-config/files/userlist/users.login ; \
echo $'\n\
DEFAULT Auth-Type == PAP\n\
        Tunnel-Type := 13 , Tunnel-Medium-Type := 6 , Tunnel-Private-Group-Id := "default_seg",\n\
        Fall-Through = Yes\n\
9cb6541e0363 Cleartext-Password := "9cb6541e0363" , NAS-Port-Type == Ethernet\n\
' > /etc/raddb/mods-config/files/userlist/mabs ; \
systemctl enable radiusd 
CMD [ "/usr/sbin/init" ]

Dockerfileからイメージをビルド

Dockerfileが作成できたら、ビルドします。

# docker build --force-rm -t infraserv:radius . -f ./radius.df

もし、dnfでコケたら、以下のコマンドが効くかもしれない。

# firewall-cmd --add-masquerade --permanent
# firewall-cmd --reload

コンテナ生成

--privileges はなんか評判悪そう(?)なので、推奨されてるぽい方法でコンテナ生成します。

# docker run --cap-add sys_admin --security-opt seccomp:unconfined -it -d --name radius -v /sys/fs/cgroup:/sys/fs/cgroup:ro -p 1812:1812/udp infraserv:radius

firewallでポート許可

radiusサービスを許可

# firewall-cmd --add-service=radius --zone=public --permanent
# firewall-cmd --reload

接続テスト

Dockerホストと別ホストから接続テストしておきます。

# radtest  hoge fuga 10.254.10.251 123 RADIUS_SECRET
Sent Access-Request Id 41 from 0.0.0.0:36748 to 10.254.10.251:1812 length 74
        User-Name = "hoge"
        User-Password = "fuga"
        NAS-IP-Address = 10.254.10.101
        NAS-Port = 123
        Message-Authenticator = 0x00
        Cleartext-Password = "fuga"
Received Access-Accept Id 41 from 10.254.10.251:1812 to 0.0.0.0:0 length 51
        Service-Type = NAS-Prompt-User
        Cisco-AVPair = "shell:priv-lvl=15"

OKですね。

実環境にあわせて調整

ログインユーザがテストユーザになっているので、修正します。
MACアドレスバイパスの対象も同様です。
コンテナにログインして、ファイル修正していきます。

# docker exec -it radius /bin/bash

radiusシークレットの修正

以下の secret = RADIUS_SECRET 行を、自身の環境にあわせてください。

/etc/raddb/clients.conf
client radius_clients {
    ipaddr  = 172.17.0.1
    netmask = 32
    secret  = RADIUS_SECRET
}

ログインユーザの登録

MD5-Passwordは echo -n PASSWORD | openssl md5 で生成できます。
Ciscoのenableは \$enab15\$ というユーザ名で試行されます。
ログイン時に自動でenableになるには、パスワードの下の行に Service-TypeとCisco-AVPairを設定します。
(priv-lvl=1が通常ユーザ、priv-lvl=15が特権ユーザ、のことです)

/etc/raddb/mods-config/files/userlist/users.login
foo       Auth-Type := PAP , Cleartext-Password := "bar"
$enab15$  Auth-Type := PAP , MD5-Password := "c32ec965db3295bad074d2afa907b1c3"
hoge      Auth-Type := PAP , MD5-Password := "c32ec965db3295bad074d2afa907b1c3"
          Service-Type := NAS-Prompt-User , Cisco-AVPair := "shell:priv-lvl=15"

MACアドレスバイパスの登録

ユーザ名とパスワードにMACアドレスを記載します。(ハイフンやコロンは削除します)
NAS-Port-Typeのアトリビュートをつけないと、機器へログインする際にUID/PWDをMACアドレスにしてログインできてしまいます。注意。

DEFAULT Auth-Type == PAP
        Tunnel-Type := 13 , Tunnel-Medium-Type := 6 , Tunnel-Private-Group-Id := "default_seg",
        Fall-Through = Yes
055dc061bf92 Cleartext-Password := "055dc061bf92" , NAS-Port-Type == Ethernet

設定ファイルを修正したら、サービスを再起動しておきます。

# systemctl restart radiusd

Cisco機器の設定

enableになって、conf tしてから以下の設定をいれていきます。
ログイン認証とMACアドレスバイパスの設定を同時に実施します。

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!  共通設定
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!aaaの採用
aaa new-model
aaa session-id common

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!  ログイン認証の設定
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!radiusサーバグループの作成とログイン認証を実施
aaa group server radius ForLogin
 server-private 10.254.10.251 auth-port 1812 acct-port 1813 timeout 1 retransmit 1 key RADIUS_SECRET

!ログイン認証、イネーブル認証、権限アトリビュートを利用する設定
aaa authentication login default group ForLogin local-case
aaa authentication enable default group ForLogin enable
aaa authorization  exec default group ForLogin if-authenticated

!ローカルアカウントはradiusサーバと疎通が取れない場合に利用される
enable secret ENABLE_PASSWORD
username LOCAL_UID password LOCAL_PWD

!ログイン時にRADIUSを利用するための設定
line vty 0 4
 login authentication ForLogin


!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!!!  MACアドレスバイパスの設定
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!ダイナミックVLANのアトリビュートで飛んでくる文字列
vlan 2022
 name default_seg

!radiusサーバグループの作成と802.1X認証を実施
aaa group server radius ForDot1X
 server-private 10.254.10.251 auth-port 1812 acct-port 1813 timeout 1 retransmit 1 key RADIUS_SECRET

!MACアドレスバイパス、ダイナミックVLAN用のアトリビュートを有効化
dot1x system-auth-control
aaa authentication dot1x default group ForDot1X
aaa authorization network default group ForDot1X if-authenticated

!interfaceへのMAB適用
interface Giga0/3
 description ## AuthPort : mac address bypass ##
 switchport mode access
 authentication order mab
 authentication priority mab
 authentication port-control auto
 mab
 dot1x pae authenticator
 spanning-tree portfast

end

念のため、ログインしたセッションは維持しつつ、別ターミナルでログインします。
ログイン出来たら、動作チェック。

radiusdのログ
各ユーザでのログインOKのログ、MACアドレスでのログインOK(=MABがOK)が記録されています。

Sat Feb  8 15:48:24 2020 : Auth: (0) Login OK: [hoge] (from client radius_clients port 35)
Sat Feb  8 15:48:53 2020 : Auth: (1) Login OK: [foo] (from client radius_clients port 35)
Sat Feb  8 15:48:56 2020 : Auth: (2) Login OK: [$enab15$] (from client radius_clients port 35 cli 10.254.10.101)
Sat Feb  8 15:49:04 2020 : Auth: (3) Login OK: [hoge] (from client radius_clients port 35)
Sat Feb  8 15:49:49 2020 : Auth: (4) Login OK: [9cb6541e0363] (from client radius_clients port 50003 cli 9C-B6-54-1E-03-63)

Ciscoのログ(MAB)
設定したGi0/3で、当該MACアドレスによるMABのSUCCESS、VLAN2022の割り当てがされています。
(ログからは読み取れませんが、アトリビュート文字列の default_seg がvlan nameの文字列とマッチして2022が割り当てられています)

Feb  9 00:49:48: %LINK-3-UPDOWN: Interface GigabitEthernet0/3, changed state to up
Feb  9 00:49:49: %AUTHMGR-5-START: Starting 'mab' for client (9cb6.541e.0363) on Interface Gi0/3 AuditSessionID 000000000000000520203B8C
Feb  9 00:49:49: %MAB-5-SUCCESS: Authentication successful for client (9cb6.541e.0363) on Interface Gi0/3 AuditSessionID 000000000000000520203B8C
Feb  9 00:49:49: %AUTHMGR-7-RESULT: Authentication result 'success' from 'mab' for client (9cb6.541e.0363) on Interface Gi0/3 AuditSessionID 000000000000000520203B8C
Feb  9 00:49:49: %AUTHMGR-5-VLANASSIGN: VLAN 2022 assigned to Interface Gi0/3 AuditSessionID 000000000000000520203B8C
Feb  9 00:49:50: %AUTHMGR-5-SUCCESS: Authorization succeeded for client (9cb6.541e.0363) on Interface Gi0/3 AuditSessionID 000000000000000520203B8C
Feb  9 00:49:51: %LINEPROTO-5-UPDOWN: Line protocol on Interface Vlan2022, changed state to up
Feb  9 00:49:51: %LINEPROTO-5-UPDOWN: Line protocol on Interface GigabitEthernet0/3, changed state to up

Ciscoのログ(AAA)

#sh aaa servers
(~~省略~~)
RADIUS: id 3, priority 0, host 10.254.10.251, auth-port 1812, acct-port 1813
     State: current UP, duration 1177s, previous duration 0s
     Dead: total time 0s, count 0
     Quarantined: No
     Authen: request 7, timeouts 1, failover 0, retransmission 1
             Response: accept 4, reject 2, challenge 0
             Response: unexpected 1, server error 0, incorrect 0, time 467ms
             Transaction: success 6, failure 0
             Throttled: transaction 0, timeout 0, failure 0
(~~省略~~)

OKですね。

ちなみに、dockerコンテナでログをみたくて、tailfしようとしたらコマンドがなかったので、watchしました。
-dは変化のあった箇所を白黒反転させる。-n 1は1秒おきに表示を更新させる。です。

# watch -d -n 1 'tail /var/log/radius/radius.log'
Every 1.0s: tail /var/log/radius/radius.log                                                      25dda9131ea4: Sat Feb  8 15:21:53 2020

Sat Feb  8 14:06:01 2020 : Warning: Ignoring "sql" (see raddb/mods-available/README.rst)
Sat Feb  8 14:06:01 2020 : Warning: Ignoring "ldap" (see raddb/mods-available/README.rst)
Sat Feb  8 14:06:01 2020 : Info: Loaded virtual server default
Sat Feb  8 14:06:01 2020 : Info:  # Skipping contents of 'if' as it is always 'false' -- /etc/raddb/sites-enabled/inner-tunnel:331
Sat Feb  8 14:06:01 2020 : Info: Loaded virtual server inner-tunnel
Sat Feb  8 14:06:01 2020 : Info: Ready to process requests
Sat Feb  8 14:06:11 2020 : Auth: (0) Login OK: [hoge] (from client radius_clients port 123)
Sat Feb  8 15:14:31 2020 : Auth: (1) Login OK: [hoge] (from client radius_clients port 123)
Sat Feb  8 15:21:38 2020 : Auth: (2) Login OK: [hoge] (from client radius_clients port 123)
Sat Feb  8 15:21:40 2020 : Auth: (3) Login OK: [hoge] (from client radius_clients port 123)

802.1X EAP-TLS 認証

無線LANのEAP-TLSはCisco WLC と FreeRADIUS を利用した EAP-TLS認証で記載しています。

さいごに

実は、 --privileges で起動したあと systemctl start radius で権限がなんちゃら~、でサービスが起動せずにハマっていました。

出典

FreeRADIUSとCisco機器を利用したログイン認証
https://hub.docker.com/r/centos/systemd/
https://unix.stackexchange.com/questions/452249/docker-container-with-centos-7-and-systemd
https://stackoverflow.com/questions/33439230/how-to-write-commands-with-multiple-lines-in-dockerfile-while-preserving-the-new

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む