Skip to content

Instantly share code, notes, and snippets.

@udzura
Last active April 20, 2024 00:58
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save udzura/06863c646f0bf713575dfdf7f7033865 to your computer and use it in GitHub Desktop.
Save udzura/06863c646f0bf713575dfdf7f7033865 to your computer and use it in GitHub Desktop.
SECKUN 2021/ProSec-IT 2021 コンテナ演習資料(公開版)

SECKUN 2021/ProSec-IT 2021 コンテナ演習資料(公開版)

この資料について

九州大学のSECKUN 2021/ProSec-IT(enPiT-Pro) 2021の共通カリキュラムにおいて、近藤 @udzura が担当したコンテナ概要の授業にて使用した教材です。

今回、公益性を鑑み、授業固有の連絡事項などを削除した状態で公開します。

ライセンスは Creative Commons Attribution 4.0 International Public License (CC BY 4.0) ref とします。 個人の自学、社内研修、スクールでの授業などでお使いいただけますが、内容は無保証です。

演習の進め方

  • それぞれの手順に従い、環境を構築し、手を動かしながら実践してください。
  • 途中の「演習」については、ぜひご自身で答えを考えてください。

0) 環境の構築

0-1) VirtualBoxとVagrantのインストール

Macの方は「OS X/Mac OS X」のリンク、Windowsの方(講師は基本的にWindowsをサポートできません、ご留意ください)は「Windows」のリンクからそれぞれインストーラを落とせるでしょう。

Linuxデスクトップの方: もしいらした場合、次の項 0-2) に一気にスキップして大丈夫です。

(重要)MacOS Monterey ではVirtualBoxが動作しないという報告がありました。 QEMU ベースである Lima であれば動くかもしれませんので、その手順を下記「0-1') Limaのセットアップ」に残してあります。筆者はMonterey環境がまだなく、こちらも動くとは限りません。M-1 Macの方と同様にパブリッククラウド上でLinuxのVM(x86-64)を構築することもご検討ください。

(重要)いわゆるM-1 Mac の方: VirtualBoxが動作しません。以下の方法での実践となりますが、サポートできません。

  • 後述の「0-1') Limaのセットアップ」を試してみる。サポート外です。
  • 職場や大学上のネットワーク、もしくはパブリッククラウド上でLinuxのVM(x86-64)を構築し、その中で演習を行う。ポートフォワーディングなどが必要な箇所は自力で考える。
  • UTM、もしくはQEMUを直接利用してVMを作成し、その中で演習を行う。参考情報のみ共有となります。

0-1-1) VMを立ち上げる

以下のようなVagrantfileを作成し、環境を立ち上げましょう。

Vagrant.configure("2") do |config|
  config.vm.box = "generic/ubuntu2004"
  config.vm.box_check_update = true

  config.vm.synced_folder ".", "/vagrant"

  config.vm.network "forwarded_port", guest: 8080, host: 8080
  # もし、ホストマシンの 8080 ポートを使っている場合、別の指定をしても構いません
  # config.vm.network "forwarded_port", guest: 8080, host: 18080

  config.vm.provider "virtualbox" do |vb|
    vb.cpus = 4
    vb.memory = (1024 * 4).to_s
  end
  config.vm.provision "shell", inline: <<-SHELL
    apt-get -y update
  SHELL
end

立ち上げのためのコマンドです。時間がかかります。

// Mac
$ vagrant up

// Win
PS> vagrant up

0-1-2) Linuxへのログイン

// Mac
$ vagrant ssh

// Win
PS> vagrant ssh

0-1') Limaのセットアップ

LimaはMacの場合homebrewでインストールできます。 ref: https://github.com/lima-vm/lima#installation

$ brew install lima

UbuntuのVMを作成します(講義で想定している Ubuntu 20.04 より新しいものが入りますが、問題ないはずです)。

$ curl -L https://raw.githubusercontent.com/lima-vm/lima/master/examples/ubuntu.yaml > seckun.yml
$ limactl start seckun.yml

別のターミナルで sudo tail -f $HOME/.lima/seckun/serial.log とすることでシリアルコンソールのログが見られます。起動時間が長い場合にお試しください。

今回は、Limaで立ち上げたマシンには以下のコマンドで入ることを推奨します(VirtualBoxなどのSSH経由接続に合わせたい)。

$ eval "$(limactl show-ssh seckun)"

その他の操作は、公式ドキュメントや limactl help などを参照してください。これ以降、Linuxにログイン後は、手順は同じです。

なお、ポートフォワーディングは Guest 8080 -> Host 8080 を自動で行うそうです。

トラブルシューティングなど

ホストの設定によって、名前解決がうまくいかなくなるパターンがあるようです。例えば /etc/resolv.conf を以下のような内容に変更すると解決する一時的に場合があります。 /etc/resolv.conf はOSの設定によっては自動で上書きされてしまうのでご留意ください。

nameserver 8.8.8.8

以下の手順では、すべてのコマンドは「Linux上のターミナルで」実行します。

0-2) Docker のインストール

vagrant@ubuntu2004:~$ sudo docker run hello-world

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

0-3) Ruby のインストール

# Rubyのインストール
# aptのものではなくSNAPのものを使います。3.0のはずです。
$ sudo apt remove ruby2.7 rubygems-integration
$ sudo snap install ruby --classic

以下のような表示が出ることを想定しています。

$ ruby --version
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]

1) Docker の操作

ここまでに、「Dockerのインストール」までを行ってください。

最低限のアプリケーションの作成

以下はRubyを使います(Pythonなどがいい方は、ご自身で同等のアプリケーションを考えて提案してもOKです。勉強させていただければと)。 「Rubyのインストール」を終えてください。

# プロジェクト作成
$ mkdir ~/seckun-app
$ cd ~/seckun-app
$ bundle init
# Gemfileを編集
gem "webrick"
gem "sinatra"
# app.rb を作成、以下に編集
require "sinatra"

get "/" do
  "Hello, from Docker. My Ruby version is: #{RUBY_VERSION}"
end
$ bundle install --path vendor/bundle
$ bundle exec ruby app.rb

演習 1-1 別のターミナルを開いてログインし、アクセス可能なことを確かめてください。

イメージの作成

# Dockerfile.step1 という名前で作成
FROM ruby:3

CMD ["ruby", "--version"]
# (1)
$ sudo docker build -t seckun:sample1 -f Dockerfile.step1 .
$ sudo docker run -t seckun:sample1
ruby 3.0.2p107 (2021-07-07 revision 0db68f0233) [x86_64-linux]
# (2)
$ sudo docker run -ti seckun:sample1 irb
irb(main):001:0> system 'uname -a'
Linux f7167eb6441f 5.4.0-33-generic #37-Ubuntu SMP Thu May 21 12:53:59 UTC 2020 x86_64 GNU/Linux
=> true
irb(main):002:0> exit

演習 1-2 (1) (2) で何が起こっているか説明してください。また、(1)でRubyの任意のプログラムや、外部スクリプトをコンテナ内部において実行させるにはどうすればいいですか?

2) 単一のコンテナとサービス

デーモンの作成

# Dockerfile.step2 というファイルを作成
FROM ruby:3
RUN gem install webrick

EXPOSE 8080
CMD ["ruby", "-run", "-e", "httpd"]
$ sudo docker build -t seckun:sample2 -f Dockerfile.step2 .
$ sudo docker run -d -p8080:8080 seckun:sample2
28db047f8dc29979b1d8876211fb9e2db841f1c211e73e4fdbad8ce011e0eaff

## ブラウザで localhost:8080 にアクセスする

2018-12-02 21 16 24

# ログの確認
$ sudo docker logs 28db047
[2021-10-27 10:13:01] INFO  WEBrick 1.7.0
[2021-10-27 10:13:01] INFO  ruby 3.0.2 (2021-07-07) [x86_64-linux]
[2021-10-27 10:13:01] INFO  WEBrick::HTTPServer#start: pid=1 port=8080
10.0.2.2 - - [27/Oct/2021:10:13:12 UTC] "GET / HTTP/1.1" 200 3269
- -> /
[2021-10-27 10:13:12] ERROR `/favicon.ico' not found.
10.0.2.2 - - [27/Oct/2021:10:13:12 UTC] "GET /favicon.ico HTTP/1.1" 404 281
http://localhost:8880/ -> /favicon.ico


## 止めるには
$ sudo docker stop 28db047
$ sudo docker rm 28db047

演習 2-1

  1. ps aufx コマンドなどでRubyのプロセスが存在することを確認してください。これは、シェルでrubyコマンドを立ち上げた時とどう違いますか?
  2. 今、ブラウザから見えているものは何なのか説明してください。

アプリケーションのコンテナ化

Tips: .dockerignore というファイルで、イメージに送らないパスを指定できる。キャッシュのようなものは除外するのが吉

# e.g.
Dockerfile*
vendor/*
# Dockerfile.step3 を作成
FROM ruby:3

RUN mkdir /app
ADD . /app
WORKDIR /app

RUN gem install bundler
RUN bundle install --deployment --path vender/bundle

EXPOSE 4567
# "-o0.0.0.0" については発展課題で
CMD ["bundle", "exec", "ruby", "app.rb", "-o0.0.0.0"]
$ sudo docker build -t seckun:sample3 -f Dockerfile.step3 .
$ sudo docker run -d -p8080:4567 seckun:sample3
fc37f85c62e4ba9c61d9b710ea8694830aca51c0e0fe2046e02c6b9f0480b15e

## ブラウザで localhost:8080 にアクセスしよう
## アクセスログを確認
$ sudo docker logs fc37f85
[2021-10-27 10:15:12] INFO  WEBrick 1.7.0
[2021-10-27 10:15:12] INFO  ruby 3.0.2 (2021-07-07) [x86_64-linux]
== Sinatra (v2.1.0) has taken the stage on 4567 for development with backup from WEBrick
[2021-10-27 10:15:12] INFO  WEBrick::HTTPServer#start: pid=1 port=4567
10.0.2.2 - - [27/Oct/2021:10:15:31 +0000] "GET / HTTP/1.1" 200 45 0.0047
10.0.2.2 - - [27/Oct/2021:10:15:31 UTC] "GET / HTTP/1.1" 200 45
- -> /
10.0.2.2 - - [27/Oct/2021:10:15:31 +0000] "GET /favicon.ico HTTP/1.1" 404 469 0.0007
10.0.2.2 - - [27/Oct/2021:10:15:31 UTC] "GET /favicon.ico HTTP/1.1" 404 469
http://localhost:8880/ -> /favicon.ico
  • 次の演習に入る前にアプリケーションは必ず停止してください。

演習 2-2

  1. 演習2-1の回答を踏まえて、今、ブラウザやcurlでアクセスできるアプリケーションがどこにいるのか説明してください。
  2. Sinatraはデフォルトで 127.0.0.1 にバインドしますが、Dockerを利用した場合それだとホストからうまくアクセスできず、 0.0.0.0 に明示的にバインド指定をする必要があります。それはなぜなのか考察してください
  3. アプリケーションの中身を以下のように変えたい場合、どういう手順を踏めばいいか説明してください。
require "sinatra"

get "/" do
  "Hello, from Docker. My Ruby version is: #{RUBY_VERSION}"
end

get "/hello" do
  "This is a new contents."
end

3) Docker Compose

ここまでの演習で作成したアプリケーションに以下のように手を加えたいと思います。

  1. アクセスカウンターを付与する
  2. アクセスカウンターのデータは、PostgreSQLに保存する

アプリケーションの変更

# Gemfileの変更
gem "webrick"
gem "sinatra"
gem "sequel"
gem "pg"

Gemfile があるディレクトリで、 bundle install をしなおします。

$ sudo apt install build-essential libpq-dev
$ bundle install

その後、アプリケーションを変更します。

# app.rb の変更
require "sinatra"
require "sequel"
require "pg"
require "logger"

DB = Sequel.postgres(ENV['POSTGRES_DB'],
       user: ENV['POSTGRES_USER'], 
       password: ENV['POSTGRES_PASSWORD'],
       host: ENV['POSTGRES_HOST'],
       port: 5432, 
       max_connections: 10,
       logger: Logger.new(STDOUT))

unless DB.table_exists?(:counters)
  DB.create_table :counters do
    primary_key :id
    Integer :count
  end
end

counters = DB[:counters]
if counters.count == 0
  counters.insert(count: 0)
end

helpers do
  def counters
    DB[:counters]
  end

  def increment_count
    counters.update(count: Sequel[:count] + 1)
  end

  def current_count
    counter = counters.first
    counter[:count]
  end
end

get "/" do
  increment_count
  "Hello, from Docker.\n" +
    "My Ruby version is: #{RUBY_VERSION}\n" +
    "Access Count: #{current_count}\n"
end
# Dockerfile.app を作成
FROM ruby:3

RUN apt -y install libpq-dev
RUN gem install bundler

RUN mkdir /app
ADD . /app
WORKDIR /app

RUN bundle install --deployment --path vender/bundle

EXPOSE 4567
CMD ["bundle", "exec", "ruby", "app.rb", "-o0.0.0.0"]
$ sudo docker build -t seckun:app -f Dockerfile.app .

演習 3-1

  • アプリケーションの実装上の変更点の概要を説明してください。

Docker Composeのインストール

Docker Composeベースのアプリケーションデプロイ

  • docker-compose.yml を作成して、PostgreSQLをバックエンドにしたWebアプリケーションを立ち上げてください。
  • YAMLは以下を参考にしても構いません。 ????? には適切な値が入りますので考える・調査すること。
app:
  image: seckun:app
  command: bundle exec ruby app.rb -o0.0.0.0
  ports:
    - 8080:4567
  environment:
    - POSTGRES_DB=seckun
    - POSTGRES_USER=postgres
    - POSTGRES_PASSWORD=testpassw0rd
    - POSTGRES_HOST=?????
  links: ?????
db:
  image: postgres:?????
  environment:
    - POSTGRES_PASSWORD=testpassw0rd
    - POSTGRES_DB=seckun

演習 3-2

  • 完成したYAMLファイルの例を提示してください。

以下の内容は、今回は時間がないので授業ではやりません(すいません)。ご興味があれば手順をさらってみましょう!

4) Kubernetesへの変換

(注意: 今回の演習の範囲ではありませんが、去年の手順をそのまま残します。細かい差異があると思いますが、勉強したい方は挑戦しても構いません)

  1. で作成したDocker Composeベースのアプリケーションは、Kubernetesのマニフェストに変更できます。

minikubeによるKubernetes環境の準備

$ USER=udzura
$ sudo usermod -aG docker $USER && newgrp docker
$ minikube start --driver=docker
  • インストールの成功を確認する
$ kubectl cluster-info
Kubernetes master is running at https://192.168.49.2:8443
KubeDNS is running at https://192.168.49.2:8443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

$ kubectl get pods --all-namespaces
NAMESPACE     NAME                               READY   STATUS    RESTARTS   AGE
kube-system   coredns-f9fd979d6-sbxhg            1/1     Running   0          33s
kube-system   etcd-minikube                      0/1     Running   0          38s
kube-system   kube-apiserver-minikube            1/1     Running   0          38s
kube-system   kube-controller-manager-minikube   0/1     Running   0          37s
kube-system   kube-proxy-fvjcj                   1/1     Running   0          33s
kube-system   kube-scheduler-minikube            0/1     Running   0          38s
kube-system   storage-provisioner                1/1     Running   0          37s

kompose によるdocker-compose.ymlの変換

$ kompose convert
INFO Kubernetes file "app-service.yaml" created   
INFO Kubernetes file "app-deployment.yaml" created 
INFO Kubernetes file "db-deployment.yaml" created 
$ mkdir manifests; mv *.yaml manifests

デプロイと調整

  • デプロイする。
$ kubectl apply -f manifests
  • Podが作成されるので状態を確認する。
$ kubectl get pods
NAME                   READY   STATUS             RESTARTS   AGE
app-6698dc95cb-kz7bx   0/1     ImagePullBackOff   0          53s
db-c495495dd-px9fq     1/1     Running            0          53s
  • ImagePullBackOff であり、イメージが見えない状態。minikube VM上のdockerでイメージをビルドする
$ eval $(minikube docker-env)
$ docker ps # 変化しているのを確認
$ docker build -t seckun:app -f Dockerfile.app .
  • 一度podを削除すると、deploymentの定義により再作成されるので、再確認する
$ kubectl get pods
NAME                   READY   STATUS    RESTARTS   AGE
app-6698dc95cb-4fmkk   0/1     Error     3          58s
db-c495495dd-px9fq     1/1     Running   0          5m19s
$ kubectl logs app-6698dc95cb-4fmkk 
/app/vender/bundle/ruby/2.7.0/gems/sequel-5.38.0/lib/sequel/adapters/postgres.rb:210:in `initialize': PG::ConnectionBad: could not translate host name "db" to address: Name or service not known (Sequel::DatabaseConnectionError)
  • 次はpostgresのホストの名前解決ができない。
    • 本来はpostgresのサービスをKubernetesで定義すべきだが、今回は簡易的に現在のdbに振られたIPを指定する。
$ kubectl describe pods/db-c495495dd-px9fq
Name:         db-c495495dd-px9fq
# ...
IP:           172.17.0.4
IPs:
  IP:           172.17.0.4
$ vim manifests/app-deployment.yaml #...
$ kubectl apply -f manifests/app-deployment.yaml
deployment.apps/app configured
$ kubectl get pods
NAME                  READY   STATUS    RESTARTS   AGE
app-fdf986cc8-4nkdv   1/1     Running   0          10s
db-c495495dd-px9fq    1/1     Running   0          12m

ブラウザでの確認

  • サービスは正しく立ち上がるのだが、実はこのままではアクセスができない。
  • services/app の定義を変更する必要がある。 type: NodePort としてexpose
apiVersion: v1
kind: Service
...
spec:
  ports:
    - name: "8080"
      nodePort: 30080
      port: 8080
      targetPort: 4567
  type: NodePort
  selector:
    io.kompose.service: app
...
$ kubectl apply -f manifests/app-service.yaml
service/app created
$ minikube service app --url
http://192.168.49.2:30080
  • このURLはホストからアクセス可能。
$ curl http://192.168.49.2:30080/
Hello, from Docker.
My Ruby version is: 2.7.2
Access Count: 1

この後は、公式ドキュメントを参考に、マニフェストの定義を色々と変えて試してみよう。


Question for learners

考えてみてください。よろしければ、お答えを @udzura に教えてください。

  • 午前の部分で特に興味が湧いた・印象に残ったトピックと、その背景を教えてください(140字以上、上限は何文字でも構いません)。

Advanced questions

  • 以下の3つのうちどれかを選択し答えてみましょう。

A) コンテナのデプロイ

  1. Google Cloudの「Cloud Run」というサービスについて調査せよ。
  2. ここまでの演習で作成したDocker単体と、Docker Composeベースのアプリケーションについて、どのアプリケーションがCloud Runへのデプロイに向いているか考察せよ。その際、デプロイできるようにするにはどのような変更を行えばいいか示せ。
  3. 他のクラウドベンダー、PaaSで類似のサービスがないか調査しまとめよ。

B) Kubernetesの動作

  1. 演習 4) で掲示した手順を、最新のバージョンにキャッチアップしつつ実行せよ(注: 講師の手抜きではない...はず?)。
  2. 生成されたYAMLマニフェストをもとに、Kubernetesの基本的なリソース Pod、Service、Deployment についてそれぞれ、関係を含め説明せよ。
  3. 成果物となるKubernetesのマニフェストファイルや、そのほか関連するDockerfileなどを一通りプロジェクトにまとめ、Gitリポジトリにし、公開されたGitHubのリポジトリにpushして提出せよ。この際、機密情報などは各自で考慮すること。
  4. (高度です)minikubeで生成されたKubernetesクラスタは単一のノードしか持たないが、ノードが複数になった場合はどのように運用が変わるか答えよ。

C) コンテナの正体に関する考察

  1. 「Linuxコンテナはただのプロセスと同じものだ」という主張に対して、根拠となる事実やLinuxの機能を挙げよ。また、もしこの主張に対する反論があればそれらも根拠を元に挙げよ。

2018-12-02 16 03 13

Appendix-1) 演習の参考資料(元となったもの)

Appendix-2) 参考資料リンク(スライドで紹介したのもの)



Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment