1
1

【docker compose構築】AlpineLinuxコンテナでansible環境構築効率化

Last updated at Posted at 2024-03-09

目的

  • 自宅PCの仮想マシンに対して、Dockerコンテナを導入し、コンテナ内でansible環境使用するのが目的となります。
  • また、仮想マシンが壊れることも想定し、Dockerfile、YAMLファイルとしてテンプレート化することで、迅速なコンテナ再構築をするのも目的です

内容

  • 前提として下記スペックで構築します。
    • コンテナベースイメージ
      • AlpineLinux3.17
      • AlpineLinux3.17からDockerfileで定義したインストールパッケージを導入
    • 自宅PCの仮想マシン(ホストOS)
      • CentOS7.5
      • メモリ3GB
      • プロセッサコア数:2
      • ハードディスク:30GB
      • ネットワークアダプタ:2つの仮想NATを使用
  • イメージを図示すると以下になります。今回は、
    • Ansible Container 1台
    • Worker Container 3台
      • nginxとcurlをansibleで導入します
    • work/hostsにノード追記すれば、拡張も可能です
  • 構築の流れとしては、下記の通りです

    • ansibleコンテナのDockerfileを作成
    • WorkerコンテナのDockerfileを作成
    • docker-composeのマニフェストファイル作成・デプロイ
    • SSH接続設定構築(自動化含む)
    • ansible実行前の追加パッケージ導入(自動化含む)
    • ansible実行(自動化含む)
  • また、自動化のため、bashスクリプトを作成しています。

ansible環境構築用の各種ファイル作成

composeansible/
├── ansible/
│   ├── Dockerfile
│   └── ansible.cfg
├── keyfiles/
│   └── _tmp_authorized_keys
├── node/
│   └── Dockerfile
└── work/
    ├── playbook.yml
    └── hosts

AnsibleDocker の設定

  • Dockerfile設定
[root@t_kyn040 composeansible]# cat ansible/Dockerfile
# alpine3.17
FROM alpine:3.17

# apk update
RUN apk update && \
    apk upgrade && \
    apk add --no-cache openssh ansible sshpass openssl bash sudo curl && \
    rm -rf /var/cache/apk/*

# keypairgenerations
RUN ssh-keygen -t rsa -b 4096 -f /root/.ssh/id_rsa -N ''
RUN mkdir /ansible_keys
RUN cp /root/.ssh/id_rsa.pub /ansible_keys/authorized_keys
RUN chmod 600 /ansible_keys/authorized_keys

# Settings
ADD ansible.cfg /etc/ansible/ansible.cfg

# entrypoint.sh
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

#ENTRYPOINT ["/entrypoint.sh"]

[root@t_kyn040 composeansible]#
  • ansible.cfg設定
    インベントリファイルとSSH接続時に使用されるSSHオプションを定義します。
    known_hosts への記載チェックをしないよう(SSH接続不可回避)設定を入れています。
[root@t_kyn040 composeansible]# cat ansible/ansible.cfg
[defaults]
inventory = /root/work/hosts

[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null

[root@t_kyn040 composeansible]#

WorkerDocker の設定

[root@t_kyn040 composeansible]# cat node/Dockerfile
# alpine3.17
FROM alpine:3.17

# install ssh server
#RUN yum -y install openssh-server && yum clean all
RUN apk update && \
    apk upgrade && \
    apk add --no-cache openssh openssl bash ca-certificates sudo python3 && \
    rm -rf /var/cache/apk/*

# create Public key
RUN ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa
RUN ssh-keygen -f /etc/ssh/ssh_host_ecdsa_key -N '' -t ecdsa
RUN mkdir /root/.ssh
RUN chmod 700 /root/.ssh

# set to login as root
RUN sed -ri 's/^#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config

# set a password for root
RUN echo "root:unkorelt_password" | chpasswd

# define args
# ENV SSHD_CONFIG_FILEPATH /etc/ssh/sshd_config
# ENV SSH_PUBKEY_FILEPATH /conf/id_key.pub
# ENV SSH_AUTHKEYS_FILEPATH /root/.ssh/authorized_keys

# copy custom scripts
#COPY run.sh /run.sh
#RUN chmod +x /run.sh

# expose 22 port
EXPOSE 22

# Entrypoint
#ENTRYPOINT ["/run.sh"]

# start up sshd
#CMD ["/usr/sbin/sshd"]
CMD ["/usr/sbin/sshd", "-D"]

[root@t_kyn040 composeansible]#

インベントリ・playbook定義

  • インベントリホストファイル
    (作りたいコンテナのホスト名を定義します。)
[root@t_kyn040 composeansible]# cat work/hosts
[node]
node01
node02
node03

[root@t_kyn040 composeansible]#
  • playbook用ymlファイル
    今回はnginx、curlをインストールします。
[root@t_kyn040 composeansible]# cat work/playbook.yml
---
- name: deploy nginx server
  hosts: node
  become: yes
  gather_facts: no
  tasks:
    - name: install nginx
      apk:
        name: nginx
        state: latest

    - name: install curl
      apk:
        name: curl
        state: latest

    - name: Start & enable NGINX service on Alpine Linux
      command:
        cmd: "/usr/sbin/nginx"
      become: true

[root@t_kyn040 composeansible]#

docker-compose.ymlの内容

  • ansibleコンテナでplaybookとインベントリhostsファイルを参照するため、ホストOS側の./workとコンテナ側の/root/workとをvolumeマウントします。
  • node01で、イメージを作成します。
  • node02、node03で「image: composeansible_node01」を定義することで、
    イメージを再利用します。(ディスク容量を節約することができます)
[root@t_kyn040 composeansible]# cat docker-compose.yml
version: "3.3"

services:
  ansible:
    container_name: 'ansible'
    build: ./ansible
    tty: true
    working_dir: '/root/work'
    volumes:
      - ./work:/root/work

  node01:
    container_name: node01
    build: ./node
    #image: composeansible_node01
    privileged: true
    #command: /sbin/init
    tty: true
    ports:
      - '8081:80'

  node02:
    container_name: node02
    image: composeansible_node01
    privileged: true
    #command: /sbin/init
    tty: true
    ports:
      - '8082:80'

  node03:
    container_name: node03
    image: composeansible_node01
    privileged: true
    #command: /sbin/init
    tty: true
    ports:
      - '8083:80'

[root@t_kyn040 composeansible]#

コンテナデプロイ

[root@t_kyn040 composeansible]# cat 00_docker_container_build.sh
#!/bin/bash

# docker container build
for filename in docker-compose.yml
do
echo =====starting deploy by ${docker-compose.yml}=====
docker-compose -f ${filename} up -d
done
cat 01_generate_keyfiles.sh
[root@t_kyn040 composeansible]# cat 01_generate_keyfiles.sh
#!/bin/bash

# 鍵持ち出し
for cntcontrol in ansible
do
echo =====before_${cntcontrol}=====
docker-compose exec ${cntcontrol} ls -l /root/.ssh/id_rsa.pub
docker-compose exec ${cntcontrol} cat /root/.ssh/id_rsa.pub
echo =====before_hostos=====
ls -l ./keyfiles/
echo =====before_${cntcontrol}=====
docker cp ${cntcontrol}:/root/.ssh/id_rsa.pub ./keyfiles/_tmp_authorized_keys
echo =====after_${cntcontrol}=====
docker-compose exec ${cntcontrol} ls -l /root/.ssh/id_rsa.pub
docker-compose exec ${cntcontrol} cat /root/.ssh/id_rsa.pub
docker-compose exec ${cntcontrol} md5sum /root/.ssh/id_rsa.pub
echo =====after_hostos=====
ls -l ./keyfiles/
done

# 鍵ファイル配布
for cnt in `cat work/hosts | grep ^node`
do
echo =====before_${cnt}=====
docker-compose exec ${cnt} ls -ld /root/.ssh
docker-compose exec ${cnt} ls -l /root/.ssh/
echo =====dist_${cnt}=====
docker cp ./keyfiles/_tmp_authorized_keys ${cnt}:/root/.ssh/authorized_keys
docker-compose exec ${cnt} chmod 600 /root/.ssh/authorized_keys
echo =====after_${cnt}=====
docker-compose exec ${cnt} ls -ld /root/.ssh
docker-compose exec ${cnt} ls -l /root/.ssh/
docker-compose exec ${cnt} md5sum /root/.ssh/authorized_keys
done

#known_hosts初回登録
for cnt in `cat work/hosts | grep ^node`
do
docker-compose exec ansible ssh -o StrictHostKeyChecking=no ${cnt} hostname
done
  • ansibleに必要な追加パッケージ導入
    コンテナビルド時にインストールでいいのではと思うかもしれないですが、
    ansibleを利用以外でpythonを使用しない場合、
    アンインストールも可能にできるよう、このタイミングで入れています。
[root@t_kyn040 composeansible]# cat 10_pre_install_package.sh
#!/bin/bash

# Ansible ping checking
for cnt in `cat work/hosts | grep ^node`
do
echo =====install adding packages into ${cnt}=====
docker-compose exec ${cnt} apk add python3
done
  • 90_check_ansible_exec.sh
    ansibleが利用可能か、pingチェックします。
[root@t_kyn040 composeansible]# cat 90_check_ansible_exec.sh
#!/bin/bash

# Ansible ping checking
for cnt in `cat work/hosts | grep ^node`
do
docker-compose exec ansible ansible ${cnt} -m ping
done
  • 95_finalcheck_ansible.sh
    playbook.ymlの内容に沿って、各コンテナのインストール状況をチェックします。
[root@t_kyn040 composeansible]# cat 95_finalcheck_ansible.sh
#!/bin/bash

# Ansible exec checking
for cntcontrol in ansible
do
docker-compose exec ${cntcontrol} ansible-playbook -i hosts playbook.yml --check
done
  • 97_commit_single_ansible.sh
    まず、1台だけ導入します。
[root@t_kyn040 composeansible]# cat 97_commit_single_ansible.sh
#!/bin/bash

# Ansible exec checking
cnt=`cat work/hosts | grep ^node | head -1`
for cntcontrol in ansible
do
docker-compose exec ${cntcontrol} ansible-playbook -i hosts -l ${cnt} playbook.yml
done
  • 99_commit_ansible.sh
    全台導入します。
[root@t_kyn040 composeansible]# cat 99_commit_ansible.sh
#!/bin/bash

# Ansible exec checking
for cntcontrol in ansible
do
docker-compose exec ${cntcontrol} ansible-playbook -i hosts playbook.yml
done

もし削除したい場合は下記で対処

  • xx_docker_container_delete.sh
    コンテナ削除します。
[root@t_kyn040 composeansible]# cat xx_docker_container_delete.sh
#!/bin/bash

# docker container stop and delete
for filename in docker-compose.yml
do
echo =====stop and delete container by ${docker-compose.yml}=====
docker-compose -f ${filename} down
done
[root@t_kyn040 composeansible]#
  • xx_docker_image_delete.sh
    コンテナイメージ削除します。
[root@t_kyn040 composeansible]# cat xx_docker_image_delete.sh
#!/bin/bash

# docker image delete
docker images
docker rmi composeansible_node01 composeansible_ansible
docker images
[root@t_kyn040 composeansible]#

※再度00_docker_container_build.shからスタートできる状態になります。

ラッピングスクリプトの作成で効率化

  • all_delete_wrapping_docker.sh
    コンテナ削除・イメージ削除を一括
[root@t_kyn040 composeansible]# cat all_delete_wrapping_docker.sh
#!/bin/bash
./xx_docker_container_delete.sh
./xx_docker_image_delete.sh

echo =====images=====
docker images
echo =====containers=====
docker ps
echo =====http connections=====
for cnt in `cat work/hosts | grep ^node`
do
echo =====curl check ${cnt}=====
docker exec -it ansible curl http://${cnt}/test.html
done
[root@t_kyn040 composeansible]#
  • all_making_wrapping_docker.sh
    コンテナ削除・コンテナイメージの削除を一括実施します。
[root@t_kyn040 composeansible]# cat all_making_wrapping_docker.sh
#!/bin/bash
./00_docker_container_build.sh
./01_generate_keyfiles.sh
./10_pre_install_package.sh
./90_check_ansible_exec.sh
./95_finalcheck_ansible.sh
./97_commit_single_ansible.sh
./99_commit_ansible.sh
echo =====images=====
docker images
echo =====containers=====
docker ps
echo =====curl check ${cnt}=====
for cnt in `cat work/hosts | grep ^node`
do
docker exec -it ansible curl http://${cnt}/test.html
done
[root@t_kyn040 composeansible]#

実行結果

  • ./all_delete_wrapping_docker.sh
    • コンテナ削除、イメージ削除をした結果になります。
    • 時間としては、1分もかからなかったです。
[root@t_kyn040 composeansible]# ./all_delete_wrapping_docker.sh
=====stop and delete container by compose.yml=====
Stopping node03  ... done
Stopping node01  ... done
Stopping ansible ... done
Stopping node02  ... done
Removing node03  ... done
Removing node01  ... done
Removing ansible ... done
Removing node02  ... done
Removing network composeansible_default
REPOSITORY                                                           TAG          IMAGE ID       CREATED         SIZE
composeansible_node01                                                latest       6c33b0e2f1af   6 minutes ago   72.3MB
composeansible_ansible                                               latest       479ef66c56e1   6 minutes ago   398MB
Untagged: composeansible_node01:latest
Deleted: sha256:6c33b0e2f1af34a57f3f454fec605db042d58c797d1dbc5451663304a0390dd8
Deleted: sha256:50d4aab7abd1bb98d66abbc52bdc5562a56a95cc12427e98454d4dead7a3d8db
Deleted: sha256:2fc36b2b22ac2a9b9d20e434f4ec423390e126cb1a4996510fc0fc1cb1c6e37f
Deleted: sha256:b53b1127f1e918943d36833643c7a54f0d1ebb58c40c199dc21022707f59835e
Deleted: sha256:cca2453b710e6e2154ed2e3af3efa39bc85d2c6e8194551ed493224f30b66128
Deleted: sha256:e86f31da72cfefd5af631f00abfb9a903e26431f07f400bd8118f8b94fe2319f
Deleted: sha256:695db7ad9746c1141207e52d3cd0a6c54ab167e68f2d0f11bf784f3051caea02
Deleted: sha256:fa7d52ff66698367cc0c54fa0a2dea325ce7d4945c072a08413bc03497009e3c
Deleted: sha256:2f2f87eeb6915c7c29f0a469c000a920cf8cbf4abf3bd743686968df1354fbca
Deleted: sha256:bfb688233cb0ba011e7e1f2b2e36f27764ac70498dc28a7285c02251dcd4b550
Deleted: sha256:eefb16d4b610bf4c58c06e5224c3350317f544fc8dc605e08ab3f8107fd532ec
Deleted: sha256:2d6171602b71b95f1716819ee8997ed41e4341bec2511f5e0957702a551843fb
Deleted: sha256:65e5db1d122508cb6da38671e020f1d4e89eeb48c6e3119c21c02ef272098ac4
Deleted: sha256:2a42af7fe212e87eea95bfaac39e50294fae8d61455f555baba6fad5a5cb6b85
Deleted: sha256:d20d8e55f3b7bd3914678203efc0fbe94cface93d728c22b6f7aad7cb7909595
Deleted: sha256:03ebc7fa8398bfd6b3a5ec6e80fefce05ddc3a3744dc5dc20773940ff8065238
Untagged: composeansible_ansible:latest
Deleted: sha256:479ef66c56e12bc2152254c879a82aae6a2cc7a3bd028db1f66734420e81ad2a
Deleted: sha256:ef8cba7d011963733ff81294d71cdaf43952d8808e3dea6d76e6bf7a264b1eef
Deleted: sha256:3a21d4cde2dbd543d5755d1ecef1a9a334939230e5b7da42f9f761ce8b4fff8b
Deleted: sha256:42ba2859e06e9e2ce6632ed3a494f9a43ca1f245372c4fb537489a0a886d9334
Deleted: sha256:440a9cfe60e69c706ea6e05b0a39dd78d90a4eed56d299e77ecaa3232d506ee5
Deleted: sha256:bd831d899cc04b37e2544e1390ff17e9c1c64cfc74b40c6a57cf390a9d56b87b
Deleted: sha256:d8fdb98d07f0cdbe44adaa7b09bcf6385988f0077f3bb03da55668475f6f0543
Deleted: sha256:384f21f55ceec7a069c88d184cf02e3db4ff68eb1834046b79d5e0a2fe30504f
Deleted: sha256:d458f4635e0bc320081c27c8ea7600f80d6646234eec477f3fee52e9aa06f934
Deleted: sha256:738e8bd6eb879c15681e97e6fc959e13c0c3d67373de65ff8ba5abc454e9f9d2
Deleted: sha256:410e2f44bf287e4892e6366e89f2bf9701df6d72b6ea5e46ed6b2a93718f30c5
Deleted: sha256:aac764517cddf3554d9774a8bcf10d4f31672ee119ea043ae5c0c4ded964beeb
Deleted: sha256:901b8650b3b6c4da0fbb1911df2e5af759f5e5cdc92ecdbe0f177e726ab7488d
Deleted: sha256:14d0f0d15a47bfeb2f781e22cb69c463a9ab16bac8e2550cb58071e52c96db51
Deleted: sha256:bb9c9fa1a5d3f3adee1d6a636dd1291aee83bc4a7d8fe4545197c955a27cc70d
Deleted: sha256:ba34a8d8e1e8a6d14591216945790a27c801a1b8231896152efa5ee6e03dca49
REPOSITORY                                                           TAG          IMAGE ID       CREATED         SIZE
-rw-r--r--. 1 root root 743  3月 10 00:27 keyfiles/_tmp_authorized_keys
`keyfiles/_tmp_authorized_keys' を削除しました
ls: keyfiles/_tmp_authorized_keys にアクセスできません: そのようなファイルやディレクトリはありません
=====images=====
REPOSITORY                                                           TAG          IMAGE ID       CREATED         SIZE
=====containers=====
CONTAINER ID   IMAGE              COMMAND     CREATED        STATUS        PORTS     NAMES
=====http connections=====
=====curl check node01=====
Error response from daemon: No such container: ansible
=====curl check node02=====
Error response from daemon: No such container: ansible
=====curl check node03=====
Error response from daemon: No such container: ansible
[root@t_kyn040 composeansible]#
  • ./all_making_wrapping_docker.sh
    • イメージ作成・コンテナ作成をした結果になります。
    • 時間としては、3分くらいかかりました。
  • :construction: 追記:apkのパッケージインストールで
    • https://dl-cdn.alpinelinux.org/alpine/v3.17/main のURL参照時、ネットワーク接続が不安定なのか、取得失敗する場合があります。
      失敗してももう一度all_making_wrapping_docker.shを実行することで
      最終的にはansible実行までできるようになりましたが、
      こちらはとても厄介で、回避策は検討中です。)
[root@t_kyn040 composeansible]# ./all_making_wrapping_docker.sh
=====starting deploy by compose.yml=====
Creating network "composeansible_default" with the default driver
Building ansible
Step 1/9 : FROM alpine:3.17
 ---> 9ed4aefc74f6
Step 2/9 : RUN apk update &&     apk upgrade &&     apk add --no-cache openssh ansible sshpass openssl bash sudo curl &&     rm -rf /var/cache/apk/*
 ---> Running in 44234053f58e
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gz
v3.17.7-42-gfd595381fd6 [https://dl-cdn.alpinelinux.org/alpine/v3.17/main]
v3.17.7-42-gfd595381fd6 [https://dl-cdn.alpinelinux.org/alpine/v3.17/community]
OK: 17825 distinct packages available
(1/5) Upgrading musl (1.2.3-r4 -> 1.2.3-r5)
(2/5) Upgrading ca-certificates-bundle (20220614-r4 -> 20230506-r0)
(3/5) Upgrading libcrypto3 (3.0.8-r3 -> 3.0.12-r4)
(4/5) Upgrading libssl3 (3.0.8-r3 -> 3.0.12-r4)
(5/5) Upgrading musl-utils (1.2.3-r4 -> 1.2.3-r5)
Executing busybox-1.35.0-r29.trigger
OK: 7 MiB in 15 packages
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gz
(1/47) Installing libbz2 (1.0.8-r4)
(2/47) Installing libexpat (2.6.0-r0)
(3/47) Installing libffi (3.4.4-r0)
(4/47) Installing gdbm (1.23-r0)
(5/47) Installing xz-libs (5.2.9-r0)
(6/47) Installing libgcc (12.2.1_git20220924-r4)
(7/47) Installing libstdc++ (12.2.1_git20220924-r4)
(8/47) Installing mpdecimal (2.5.1-r1)
(9/47) Installing ncurses-terminfo-base (6.3_p20221119-r1)
(10/47) Installing ncurses-libs (6.3_p20221119-r1)
(11/47) Installing readline (8.2.0-r0)
(12/47) Installing sqlite-libs (3.40.1-r1)
(13/47) Installing python3 (3.10.13-r0)
(14/47) Installing py3-markupsafe (2.1.1-r1)
(15/47) Installing py3-jinja2 (3.1.2-r0)
(16/47) Installing py3-parsing (3.0.9-r0)
(17/47) Installing py3-packaging (21.3-r2)
(18/47) Installing yaml (0.2.5-r0)
(19/47) Installing py3-yaml (6.0-r0)
(20/47) Installing py3-cparser (2.21-r0)
(21/47) Installing py3-cffi (1.15.1-r0)
(22/47) Installing py3-cryptography (38.0.3-r1)
(23/47) Installing py3-asn1 (0.4.8-r2)
(24/47) Installing py3-bcrypt (4.0.1-r0)
(25/47) Installing py3-pynacl (1.5.0-r1)
(26/47) Installing py3-six (1.16.0-r3)
(27/47) Installing py3-paramiko (2.12.0-r0)
(28/47) Installing py3-resolvelib (0.8.1-r0)
(29/47) Installing ansible-core (2.13.6-r0)
(30/47) Installing ansible (6.6.0-r0)
(31/47) Installing bash (5.2.15-r0)
Executing bash-5.2.15-r0.post-install
(32/47) Installing ca-certificates (20230506-r0)
(33/47) Installing brotli-libs (1.0.9-r9)
(34/47) Installing nghttp2-libs (1.51.0-r2)
(35/47) Installing libcurl (8.5.0-r0)
(36/47) Installing curl (8.5.0-r0)
(37/47) Installing openssh-keygen (9.1_p1-r5)
(38/47) Installing libedit (20221030.3.1-r0)
(39/47) Installing openssh-client-common (9.1_p1-r5)
(40/47) Installing openssh-client-default (9.1_p1-r5)
(41/47) Installing openssh-sftp-server (9.1_p1-r5)
(42/47) Installing openssh-server-common (9.1_p1-r5)
(43/47) Installing openssh-server (9.1_p1-r5)
(44/47) Installing openssh (9.1_p1-r5)
(45/47) Installing openssl (3.0.12-r4)
(46/47) Installing sshpass (1.09-r1)
(47/47) Installing sudo (1.9.12_p2-r1)
Executing busybox-1.35.0-r29.trigger
Executing ca-certificates-20230506-r0.trigger
OK: 461 MiB in 62 packages
Removing intermediate container 44234053f58e
 ---> 4b36271a4d3b
Step 3/9 : RUN ssh-keygen -t rsa -b 4096 -f /root/.ssh/id_rsa -N ''
 ---> Running in 8bc412f82598
Generating public/private rsa key pair.
Created directory '/root/.ssh'.
Your identification has been saved in /root/.ssh/id_rsa
Your public key has been saved in /root/.ssh/id_rsa.pub
The key fingerprint is:
SHA256:yzM6xXsVpKzynw/sBPZaNdogxl7NMt4NSfy4u2/iyaE root@8bc412f82598
The key's randomart image is:
+---[RSA 4096]----+
|                 |
|           ..    |
|         . oo    |
|       .  o+.+   |
|       .S.= O..  |
|      .=+O O.=   |
|       +*.O.= .  |
|      ..oB.=.+.  |
|      ...oEoB=.  |
+----[SHA256]-----+
Removing intermediate container 8bc412f82598
 ---> f2bc2c0055d1
Step 4/9 : RUN mkdir /ansible_keys
 ---> Running in 7358dc3d1179
Removing intermediate container 7358dc3d1179
 ---> b237e9f6f648
Step 5/9 : RUN cp /root/.ssh/id_rsa.pub /ansible_keys/authorized_keys
 ---> Running in aa6423945b91
Removing intermediate container aa6423945b91
 ---> 61f7a86531b6
Step 6/9 : RUN chmod 600 /ansible_keys/authorized_keys
 ---> Running in b79aacbfb26e
Removing intermediate container b79aacbfb26e
 ---> cd8ebeb3d11f
Step 7/9 : ADD ansible.cfg /etc/ansible/ansible.cfg
 ---> 467f4b2a809b
Step 8/9 : COPY entrypoint.sh /entrypoint.sh
 ---> 21d83acb3370
Step 9/9 : RUN chmod +x /entrypoint.sh
 ---> Running in 84f0e4fb6d67
Removing intermediate container 84f0e4fb6d67
 ---> ad4f253742e8

Successfully built ad4f253742e8
Successfully tagged composeansible_ansible:latest
WARNING: Image for service ansible was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Building node01
Step 1/10 : FROM alpine:3.17
 ---> 9ed4aefc74f6
Step 2/10 : RUN apk update &&     apk upgrade &&     apk add --no-cache openssh openssl bash ca-certificates sudo python3 &&     rm -rf /var/cache/apk/*
 ---> Running in ea122400585e
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gz
v3.17.7-42-gfd595381fd6 [https://dl-cdn.alpinelinux.org/alpine/v3.17/main]
v3.17.7-42-gfd595381fd6 [https://dl-cdn.alpinelinux.org/alpine/v3.17/community]
OK: 17825 distinct packages available
(1/5) Upgrading musl (1.2.3-r4 -> 1.2.3-r5)
(2/5) Upgrading ca-certificates-bundle (20220614-r4 -> 20230506-r0)
(3/5) Upgrading libcrypto3 (3.0.8-r3 -> 3.0.12-r4)
(4/5) Upgrading libssl3 (3.0.8-r3 -> 3.0.12-r4)
(5/5) Upgrading musl-utils (1.2.3-r4 -> 1.2.3-r5)
Executing busybox-1.35.0-r29.trigger
OK: 7 MiB in 15 packages
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gz
(1/25) Installing ncurses-terminfo-base (6.3_p20221119-r1)
(2/25) Installing ncurses-libs (6.3_p20221119-r1)
(3/25) Installing readline (8.2.0-r0)
(4/25) Installing bash (5.2.15-r0)
Executing bash-5.2.15-r0.post-install
(5/25) Installing ca-certificates (20230506-r0)
(6/25) Installing openssh-keygen (9.1_p1-r5)
(7/25) Installing libedit (20221030.3.1-r0)
(8/25) Installing openssh-client-common (9.1_p1-r5)
(9/25) Installing openssh-client-default (9.1_p1-r5)
(10/25) Installing openssh-sftp-server (9.1_p1-r5)
(11/25) Installing openssh-server-common (9.1_p1-r5)
(12/25) Installing openssh-server (9.1_p1-r5)
(13/25) Installing openssh (9.1_p1-r5)
(14/25) Installing openssl (3.0.12-r4)
(15/25) Installing libbz2 (1.0.8-r4)
(16/25) Installing libexpat (2.6.0-r0)
(17/25) Installing libffi (3.4.4-r0)
(18/25) Installing gdbm (1.23-r0)
(19/25) Installing xz-libs (5.2.9-r0)
(20/25) Installing libgcc (12.2.1_git20220924-r4)
(21/25) Installing libstdc++ (12.2.1_git20220924-r4)
(22/25) Installing mpdecimal (2.5.1-r1)
(23/25) Installing sqlite-libs (3.40.1-r1)
(24/25) Installing python3 (3.10.13-r0)
(25/25) Installing sudo (1.9.12_p2-r1)
Executing busybox-1.35.0-r29.trigger
Executing ca-certificates-20230506-r0.trigger
OK: 71 MiB in 40 packages
Removing intermediate container ea122400585e
 ---> bfa653036e37
Step 3/10 : RUN ssh-keygen -f /etc/ssh/ssh_host_rsa_key -N '' -t rsa
 ---> Running in 58d1cced57d6
Generating public/private rsa key pair.
Your identification has been saved in /etc/ssh/ssh_host_rsa_key
Your public key has been saved in /etc/ssh/ssh_host_rsa_key.pub
The key fingerprint is:
SHA256:O8g1FKtukJcfVopXAqVkAiszNsN1sUfkDLf8oA+sq9g root@58d1cced57d6
The key's randomart image is:
+---[RSA 3072]----+
|  .o.+*=o        |
|. ...+Oo.o       |
|+=.  ..B+ o      |
|.+o ..o=o=       |
|    o+= S.       |
|    .=o* +       |
|   .  =.+        |
|..  ..   .       |
|..E.             |
+----[SHA256]-----+
Removing intermediate container 58d1cced57d6
 ---> f77c4f185c49
Step 4/10 : RUN ssh-keygen -f /etc/ssh/ssh_host_ecdsa_key -N '' -t ecdsa
 ---> Running in 58fa97dd390b
Generating public/private ecdsa key pair.
Your identification has been saved in /etc/ssh/ssh_host_ecdsa_key
Your public key has been saved in /etc/ssh/ssh_host_ecdsa_key.pub
The key fingerprint is:
SHA256:ZnWeYtXrj4frsQ2AFV1IJxwODnIYt84LKw/QTY4q3Do root@58fa97dd390b
The key's randomart image is:
+---[ECDSA 256]---+
|        oo+ o+++o|
|        .+ + =+o |
|        . o = o  |
|     . = + * . . |
|    . o S * + .  |
| . . o o + o o   |
|  o o o . .   +. |
|  Eo   +      .B.|
|  ..    .    .=oo|
+----[SHA256]-----+
Removing intermediate container 58fa97dd390b
 ---> 1b8aa72f0774
Step 5/10 : RUN mkdir /root/.ssh
 ---> Running in 30edda3f1bf2
Removing intermediate container 30edda3f1bf2
 ---> 542026acdd30
Step 6/10 : RUN chmod 700 /root/.ssh
 ---> Running in a75cf45c1131
Removing intermediate container a75cf45c1131
 ---> 6b369aaaf378
Step 7/10 : RUN sed -ri 's/^#PermitEmptyPasswords no/PermitEmptyPasswords yes/' /etc/ssh/sshd_config
 ---> Running in d687f8a98afd
Removing intermediate container d687f8a98afd
 ---> 37efec80adbc
Step 8/10 : RUN echo "root:unkorelt_password" | chpasswd
 ---> Running in 5c547cf0a74e
chpasswd: password for 'root' changed
Removing intermediate container 5c547cf0a74e
 ---> 336fd0599281
Step 9/10 : EXPOSE 22
 ---> Running in 1f42871e0d22
Removing intermediate container 1f42871e0d22
 ---> 85572e5dd95f
Step 10/10 : CMD ["/usr/sbin/sshd", "-D"]
 ---> Running in 519c7ab2b9d2
Removing intermediate container 519c7ab2b9d2
 ---> cd239d9ab78d

Successfully built cd239d9ab78d
Successfully tagged composeansible_node01:latest
WARNING: Image for service node01 was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`.
Creating node01 ... done
Creating node03 ...
Creating node01 ...
Creating node02 ...
=====before_ansible=====
-rw-r--r--    1 root     root           743 Mar  9 15:41 /root/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCycSMiZxSsME9eRM22dIZYGrO3/mUxYW2OVpzOAau180+9JMoAMoiS8TMOcjHDlo1IEGTJpyIpdiUi7mQsf6lezQDdrigqYeGSWG2KXAgcc6pRkx9hmXWBp/C0P7P+wacKtxrGWdL7c0GXO3vC9SA2X2Bz0CVimuClPiJFmjX/CyvWqnKrMC2qRVT07a3ljSf7+m3LPHeqc1tzsCnjvAiYDURqF2IYcWfftQJShu5P8YchfVHZ4BzS0NSVY0T21W+3rrL5Gkc2eVXGnmurX/SljJykwq8fc9GyHySrI38KaYodGhyJtMAlArzaDjiXq7hBq5jRj5ASYa3jkWYG3GL9NKQpXzE7mxBLtSJLWT+BrQ6hgwziPEKC07+l0WZaOZrbKU0GrXHBbQuPvirp6dAPdp4O7/HbinmN8ao00ReeILCtCP2MrSqQIH9aSv1j3j1c+ORGR2VrefefzmfrcsHXIZSApZzz7nhwZ3gOYiVnHdumcT6Ay2l5v9LGZnH9u/9tNPAM+8ll22RYbC3IGUTZAcH2X3qe12pXbL5+a38RBYEAwpJdKZSC9lrzINBaiwCDcEpYr9e/YaqL/c7SpBFFhQg/vmTfmJZ2I2PQJSj6TFEWAx/VXympUcnuIBARhiwGGxba94v+PCyLG8ECU/IihD3QMdHCjEbnm98+Z08sCQ== root@8bc412f82598
=====before_hostos=====
合計 0
=====before_ansible=====
Successfully copied 2.56kB to /root/composeansible/keyfiles/_tmp_authorized_keys
=====after_ansible=====
-rw-r--r--    1 root     root           743 Mar  9 15:41 /root/.ssh/id_rsa.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCycSMiZxSsME9eRM22dIZYGrO3/mUxYW2OVpzOAau180+9JMoAMoiS8TMOcjHDlo1IEGTJpyIpdiUi7mQsf6lezQDdrigqYeGSWG2KXAgcc6pRkx9hmXWBp/C0P7P+wacKtxrGWdL7c0GXO3vC9SA2X2Bz0CVimuClPiJFmjX/CyvWqnKrMC2qRVT07a3ljSf7+m3LPHeqc1tzsCnjvAiYDURqF2IYcWfftQJShu5P8YchfVHZ4BzS0NSVY0T21W+3rrL5Gkc2eVXGnmurX/SljJykwq8fc9GyHySrI38KaYodGhyJtMAlArzaDjiXq7hBq5jRj5ASYa3jkWYG3GL9NKQpXzE7mxBLtSJLWT+BrQ6hgwziPEKC07+l0WZaOZrbKU0GrXHBbQuPvirp6dAPdp4O7/HbinmN8ao00ReeILCtCP2MrSqQIH9aSv1j3j1c+ORGR2VrefefzmfrcsHXIZSApZzz7nhwZ3gOYiVnHdumcT6Ay2l5v9LGZnH9u/9tNPAM+8ll22RYbC3IGUTZAcH2X3qe12pXbL5+a38RBYEAwpJdKZSC9lrzINBaiwCDcEpYr9e/YaqL/c7SpBFFhQg/vmTfmJZ2I2PQJSj6TFEWAx/VXympUcnuIBARhiwGGxba94v+PCyLG8ECU/IihD3QMdHCjEbnm98+Z08sCQ== root@8bc412f82598
a9601735ad45e0ae29e6d80fcb99bdf0  /root/.ssh/id_rsa.pub
=====after_hostos=====
合計 4
-rw-r--r--. 1 root root 743  3月 10 00:41 _tmp_authorized_keys
=====before_node01=====
drwx------    1 root     root             6 Mar  9 15:41 /root/.ssh
total 0
=====dist_node01=====
Successfully copied 2.56kB to node01:/root/.ssh/authorized_keys
=====after_node01=====
drwx------    1 root     root            29 Mar  9 15:41 /root/.ssh
total 4
-rw-------    1 root     root           743 Mar  9 15:41 authorized_keys
a9601735ad45e0ae29e6d80fcb99bdf0  /root/.ssh/authorized_keys
=====before_node02=====
drwx------    1 root     root             6 Mar  9 15:41 /root/.ssh
total 0
=====dist_node02=====
Successfully copied 2.56kB to node02:/root/.ssh/authorized_keys
=====after_node02=====
drwx------    1 root     root            29 Mar  9 15:41 /root/.ssh
total 4
-rw-------    1 root     root           743 Mar  9 15:41 authorized_keys
a9601735ad45e0ae29e6d80fcb99bdf0  /root/.ssh/authorized_keys
=====before_node03=====
drwx------    1 root     root             6 Mar  9 15:41 /root/.ssh
total 0
=====dist_node03=====
Successfully copied 2.56kB to node03:/root/.ssh/authorized_keys
=====after_node03=====
drwx------    1 root     root            29 Mar  9 15:41 /root/.ssh
total 4
-rw-------    1 root     root           743 Mar  9 15:41 authorized_keys
a9601735ad45e0ae29e6d80fcb99bdf0  /root/.ssh/authorized_keys
Warning: Permanently added 'node01' (ECDSA) to the list of known hosts.
4d2d08a1b2a6
Warning: Permanently added 'node02' (ECDSA) to the list of known hosts.
6cbd854619d2
Warning: Permanently added 'node03' (ECDSA) to the list of known hosts.
883d28d2ea58
=====install adding packages into node01=====
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gz
OK: 71 MiB in 40 packages
=====install adding packages into node02=====
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gz
OK: 71 MiB in 40 packages
=====install adding packages into node03=====
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.17/community/x86_64/APKINDEX.tar.gz
OK: 71 MiB in 40 packages
[WARNING]: Platform linux on host node01 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
node01 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.10"
    },
    "changed": false,
    "ping": "pong"
}
[WARNING]: Platform linux on host node02 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
node02 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.10"
    },
    "changed": false,
    "ping": "pong"
}
[WARNING]: Platform linux on host node03 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
node03 | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3.10"
    },
    "changed": false,
    "ping": "pong"
}

PLAY [deploy nginx server] ********************************************************************************************************************************

TASK [install nginx] **************************************************************************************************************************************
[WARNING]: Platform linux on host node01 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
changed: [node01]
[WARNING]: Platform linux on host node02 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
changed: [node02]
[WARNING]: Platform linux on host node03 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
changed: [node03]

TASK [install curl] ***************************************************************************************************************************************
changed: [node03]
changed: [node02]
changed: [node01]

TASK [Start & enable NGINX service on Alpine Linux] *******************************************************************************************************
skipping: [node01]
skipping: [node02]
skipping: [node03]

PLAY RECAP ************************************************************************************************************************************************
node01                     : ok=2    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
node02                     : ok=2    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0
node03                     : ok=2    changed=2    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0


PLAY [deploy nginx server] ********************************************************************************************************************************

TASK [install nginx] **************************************************************************************************************************************
[WARNING]: Platform linux on host node01 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
changed: [node01]

TASK [install curl] ***************************************************************************************************************************************
changed: [node01]

TASK [Start & enable NGINX service on Alpine Linux] *******************************************************************************************************
changed: [node01]

PLAY RECAP ************************************************************************************************************************************************
node01                     : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0


PLAY [deploy nginx server] ********************************************************************************************************************************

TASK [install nginx] **************************************************************************************************************************************
[WARNING]: Platform linux on host node01 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
ok: [node01]
[WARNING]: Platform linux on host node03 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
changed: [node03]
[WARNING]: Platform linux on host node02 is using the discovered Python interpreter at /usr/bin/python3.10, but future installation of another Python
interpreter could change the meaning of that path. See https://docs.ansible.com/ansible-core/2.13/reference_appendices/interpreter_discovery.html for more
information.
changed: [node02]

TASK [install curl] ***************************************************************************************************************************************
ok: [node01]
changed: [node02]
changed: [node03]

TASK [Start & enable NGINX service on Alpine Linux] *******************************************************************************************************
changed: [node02]
changed: [node03]
fatal: [node01]: FAILED! => {"changed": true, "cmd": ["/usr/sbin/nginx"], "delta": "0:00:03.460210", "end": "2024-03-09 15:42:30.140508", "msg": "non-zero return code", "rc": 1, "start": "2024-03-09 15:42:26.680298", "stderr": "nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)\nnginx: [emerg] bind() to [::]:80 failed (98: Address in use)\nnginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)\nnginx: [emerg] bind() to [::]:80 failed (98: Address in use)\nnginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)\nnginx: [emerg] bind() to [::]:80 failed (98: Address in use)\nnginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)\nnginx: [emerg] bind() to [::]:80 failed (98: Address in use)\nnginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)\nnginx: [emerg] bind() to [::]:80 failed (98: Address in use)\nnginx: [emerg] still could not bind()", "stderr_lines": ["nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)", "nginx: [emerg] bind() to [::]:80 failed (98: Address in use)", "nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)", "nginx: [emerg] bind() to [::]:80 failed (98: Address in use)", "nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)", "nginx: [emerg] bind() to [::]:80 failed (98: Address in use)", "nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)", "nginx: [emerg] bind() to [::]:80 failed (98: Address in use)", "nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address in use)", "nginx: [emerg] bind() to [::]:80 failed (98: Address in use)", "nginx: [emerg] still could not bind()"], "stdout": "", "stdout_lines": []}

PLAY RECAP ************************************************************************************************************************************************
node01                     : ok=2    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0
node02                     : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
node03                     : ok=3    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

=====images=====
REPOSITORY                                                           TAG          IMAGE ID       CREATED              SIZE
composeansible_node01                                                latest       cd239d9ab78d   About a minute ago   72.3MB
composeansible_ansible                                               latest       ad4f253742e8   About a minute ago   398MB
=====containers=====
CONTAINER ID   IMAGE                    COMMAND               CREATED              STATUS              PORTS                                           NAMES
6cbd854619d2   composeansible_node01    "/usr/sbin/sshd -D"   About a minute ago   Up About a minute   22/tcp, 0.0.0.0:8082->80/tcp, :::8082->80/tcp   node02
ef2726c26794   composeansible_ansible   "/bin/sh"             About a minute ago   Up About a minute                                                   ansible
4d2d08a1b2a6   composeansible_node01    "/usr/sbin/sshd -D"   About a minute ago   Up About a minute   22/tcp, 0.0.0.0:8081->80/tcp, :::8081->80/tcp   node01
883d28d2ea58   composeansible_node01    "/usr/sbin/sshd -D"   About a minute ago   Up About a minute   22/tcp, 0.0.0.0:8083->80/tcp, :::8083->80/tcp   node03
=====curl check =====
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>
[root@t_kyn040 composeansible]#

あとがき

AlpineLinuxはDocker Imageのサイズが他のベースイメージよりも比較的小さいですが、パッケージの互換性が低く、
apkという他のディストリビューションにはないパッケージマネージャを使っています。
その観点から非推奨といった評価もあります。
また、Pythonモジュールとは相性が合わない記事もあり非推奨という記事もあります。
https://blog.adachin.me/archives/11686

しかしながら個人的にはメリット・デメリットがあると感じます。

  • メリット
    • 構築時にアーキテクチャを確認する面で価値はあり
    • 不要なパッケージを入れずに済むので脆弱性も少なくなる
  • デメリット
    • 特にインストールモジュールがあまりにも少なく、運用が煩雑
      • pythonが入っていない。python3を入れる
      • curlを入れる
      • root実行権限がない。sudoを導入
      • SSHができない。openssh opensslを導入
      • など、要件によっては導入パッケージを入れる必要…
    • 障害運用やメンテナンスがしにくい
      • 参考のサイト見たりしてトラブルシューティング対応が必要
  • カスタマイズ性はありましたが、考えなければいけない点が多いと実感しました…

今後

  • Webサーバだけでなく、Nodejs、MongoDBも構築し、Webサーバを構築を目指したいと思います。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1