hachiNote

勉強したことをメモします。

「初めてのサーバーレスアプリケーション開発」の記事を見て学んでみた

目的

私は現時点でクラウドサービスを使った開発について何も知らず、AWSも何も知らない状態です。でもなんか勉強してみないとなということで、ひとまずなんとなく目に入ったDynamoDBとやらについて学んで見ようと検索してみたところ、Developers IOの以下の記事が見つかりました。この記事はDynamoDBに関してのリンク集みたいな感じになっていてとても参考になります。

dev.classmethod.jp

その中にある「初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~」というのが手を動かして学べる感じになっていたので主にこちらの記事に則って学んでいくことにしました。全5回の記事です。

dev.classmethod.jp

この5つの記事をやってみて、ちょっとつまづいたところなどをメモします。

「初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~」をやってみたメモ

初めてのサーバーレスアプリケーション開発 ~DynamoDBにテーブルを作成する~ | DevelopersIO

感想

ひとまずテーブル名とプライマリキーさえ決めればテーブルは作れるようだ。プライマリキーとしてはパーティションキーというのが必須で、あとはソートキーというのも指定できるみたい。どちらのキーにも該当しないキーをテーブルにいきなり突っ込んでも問題ないようで、その辺はNoSQLだからなのかな。MongoDBでもそうっぽかったし。

つまづいたところ

手順的につまづいたところは特になかった。

「初めてのサーバーレスアプリケーション開発 ~LambdaでDynamoDBの値を取得する~」をやってみたメモ

初めてのサーバーレスアプリケーション開発 ~LambdaでDynamoDBの値を取得する~ | DevelopersIO

感想

Lambdaって断片的なコードを書いただけで後はリクエストがあったらそれが実行されるわけですが、「実行されるってどうやって? サーバや実行環境はどこにあってどうなっているのか?」と思ってました。最近Dockerをちょっと勉強していたんですが、もしかしてこういうのってコンテナ技術を使って都度実行を実現しているのかな。

使用する言語はてっきりNode.js一本かと思ったら、記事ではPython使ってました。PythonってAIのコード書くときに使うものだと思ってたんですが、サーバ側でも使うんですね。

つまづいたところ

手順的につまづいたところは特になかった。

「初めてのサーバーレスアプリケーション開発 ~API GatewayからLambdaを呼び出す~」をやってみたメモ

初めてのサーバーレスアプリケーション開発 ~API GatewayからLambdaを呼び出す~ | DevelopersIO

感想

LambdaとAPI Gatewayがなぜ別々になっているのか、一緒でもいいんじゃないかと思ったりしてましたが、Lambdaを実行するきっかけとしてはWeb API以外にもあるってことなのかな。APIごとにセキュリティ的な設定もできるみたいだし、確かに分かれていた方が設計的に汎用性が高そうです。「本文マッピングテンプレート追加」あたりは用語があまり理解できていない部分がありますが、とりあえず目的の動作はできました。

つまづいたところ

多分AWSのサービスが年々更新されていく事によって、記事内にある画面が今は違ってきているところがありましたが、よく見ればわかる程度で大丈夫でした。

「初めてのサーバーレスアプリケーション開発 ~Serverless Framework を使ってAWSリソースをデプロイする~」をやってみたメモ

初めてのサーバーレスアプリケーション開発 ~Serverless Framework を使ってAWSリソースをデプロイする~ | DevelopersIO

感想

Serverless Frameworkというフレームワークを使うことによって、これまで画面上で1つずつ作っていたAPI Gateway、Lambda、DynamoDBの設定をCLIベースでデプロイできる、つまり設定をコード化できる。ただ、急にCloudFormationという言葉が出てきたのでそこが少し混乱しました。デプロイしたらAPI Gateway、Lambda、DynamoDBにそれぞれ設定が出来上がるだけと思っていたら、CloudFormationってサービスのところにスタックというやつが現れて、API Gateway、Lambda、DynamoDBはスタックに紐づくリソース?という形になっているんですよね。ちょっとこのスタックというやつの概念はまだ理解できていません。ただ取りまとめているやつがいることで、それぞれのサービスをバラバラで管理するんじゃなくてアプリケーションの単位で管理できるし一括でremoveできるから便利だよねというのはわかります。

Cloud9というのはIDEがブラウザ上で使えるってだけじゃなくて、Linuxとかの環境ごとクラウドに用意して使うってことなんですね。なので、npm installしたりしたらそれがそのまま残っているので便利です。ただ環境作ってそのままにしておくとEC2の使用料金がかかってくるみたいです。後で消しておかないと。個人で開発するなら、自分のマシン上に環境作った方が良さそう。

つまづいたところ

記事内でデプロイコマンドとして sls deploy -v と書かれていましたが、これだとバージョン表示されるだけでデプロイできませんでした。 -v なしでいけました。

それから、 sls invoke -f hello としてLambdaファンクションを実行できるんですが、これはデプロイされたクラウド上にあるやつが実行されるのか、Cloud9上で動いているだけなのかいまいちよくわからなかった。 こちらの記事によると、ローカルで実行するには sls invoke local コマンドを使うようなのでクラウド上のやつが実行されてことは間違いないとは思います。ただ本当にクラウド上で実行されたことの確証というかログみたいなのが見つけられなかったのでちょっと心残り。 今から始めるServerless Frameworkで簡単Lambda開発環境の構築 | DevelopersIO

後は sls invoke -f hello などを実行したときに

1 deprecation found: run 'serverless doctor' for more details

と表示されることがあり、ちょっと気になってました。言われるがままに serverless doctor コマンドを打ってみる。

Administrator:~/environment/demo-service $ severless doctor
bash: severless: command not found
Administrator:~/environment/demo-service $ serverless doctor
1 deprecation triggered in the last command:

Starting with version 4.0.0, following property will be replaced:
  "provider.iamRoleStatements" -> "provider.iam.role.statements"
More info: https://serverless.com/framework/docs/deprecations/#PROVIDER_IAM_SETTINGS_V3

どうもserverless.ymlの書き方が変わりますよということみたい。最初に生成されたserverless.ymlにはコメントとして書き方の例があり、その書き方に合わせるとこの警告は出なくなった。

初めてのサーバーレスアプリケーション開発 ~Serverless Framework と Codeシリーズを連携させる~

初めてのサーバーレスアプリケーション開発 ~Serverless Framework と Codeシリーズを連携させる~ | DevelopersIO

感想

前回の記事で「serverless.ymlとかはどうやってコード管理するんだろうな」と思ってたんですが、その回答がこの記事に書いてありました。さらにコードのpushをトリガーにして自動ビルド&デプロイを行う方法が書いてあります。この辺は、GitHubGitHub Actionsに該当する機能なんですね。

手順的につまづいたところ

CodeBuildの設定画面がかなり違ってきている。 記事では、「環境イメージ」の設定で「Dockerイメージの指定」を選び、「環境タイプ」としてLinux、「カスタムイメージタイプ」としてその他を選び、「カスタムイメージID」は aws/codebuild/eb-nodejs-4.4.6-amazonlinux-64:2.1.3 とあるが、今の画面で「カスタムイメージ」を選択して「環境タイプ」でLinuxを選択するとイメージレジストリを選択するような画面になり、カスタムイメージIDを入れられるような画面はない。

結果として以下のような感じで環境イメージなどを選んでみた。

CodeBuildの「環境」の設定

「イメージのバージョン」は「このランタイムバージョンには常に最新のイメージを使用してください」のままで通ったので、多分勝手に最新のものが選ばれたのかもしれない。

個人的なミスでつまづいたところ

コードを書いてpushするとCodePipelineが動きCodeBuildのビルドが成功した。しかしログをよく見てみると、BuildのフェーズでWarnが出ている。

[Container] 2022/05/03 10:45:00 Entering phase BUILD
[Container] 2022/05/03 10:45:00 Running command echo Build started on `date`
Build started on Tue May 3 10:45:00 UTC 2022

[Container] 2022/05/03 10:45:00 Running command cd demo-cicd-service && sls deploy

Warning: Invalid configuration encountered
  at 'functions.hello.events.0': unsupported function event

Learn more about configuration validation here: http://slss.io/configuration-validation

Deploying demo-cicd-service to stage dev (us-east-2)

✔ Service deployed to stack demo-cicd-service-dev (82s)

functions:
  hello: demo-cicd-service-dev-hello (347 B)

1 deprecation found: run 'serverless doctor' for more details

[Container] 2022/05/03 10:46:27 Phase complete: BUILD State: SUCCEEDED

functionのところだから、Lambdaのところがうまくいっていないのかな? どうすれば確認できるだろう? Server FrameworkでデプロイするとCloudFormationのところにスタックというのができてたと思うから、そちらを見てみる。 スタックからリソースを確認すると、demo-cicd-service-dev-helloという関数はできているし、DynamoDBもできているので不審な点はなさそう。とりあえず動作確認を続けてみる。

いろいろ確認してみた結果、 functions.hello.events の下のインデントがずれていた。なるほど、YAMLだからインデントのズレは受け入れられないんですね。そのほかにも閉じクォーテーション忘れとか細かい入力ミスがあったので合わせて直してgit pushし直す。

すると次はビルドに失敗している模様。

[Container] 2022/05/07 06:24:34 Running command cd demo-cicd-service && sls deploy
Deploying demo-cicd-service to stage dev (us-east-2)

× Stack demo-cicd-service-dev failed to deploy (11s)
Environment: linux, node 12.22.2, framework 3.16.0, plugin 6.2.2, SDK 4.3.2
Docs:        docs.serverless.com
Support:     forum.serverless.com
Bugs:        github.com/serverless/serverless/issues
Error:
UPDATE_FAILED: DemoDynamoDbTable (AWS::DynamoDB::Table)
CloudFormation cannot update a stack when a custom-named resource requires replacing. Rename demo-sls-person and update the stack again.

View the full error: https://us-east-2.console.aws.amazon.com/cloudformation/home?region=us-east-2#/stack/detail?stackId=xxx

[Container] 2022/05/07 06:24:50 Command did not exit successfully cd demo-cicd-service && sls deploy exit status 1
[Container] 2022/05/07 06:24:50 Phase complete: BUILD State: FAILED
[Container] 2022/05/07 06:24:50 Phase context status code: COMMAND_EXECUTION_ERROR Message: Error while executing command: cd demo-cicd-service && sls deploy. Reason: exit status 1
[Container] 2022/05/07 06:24:50 Entering phase POST_BUILD
[Container] 2022/05/07 06:24:50 Running command echo Build completed on `date`
Build completed on Sat May 7 06:24:50 UTC 2022

[Container] 2022/05/07 06:24:50 Phase complete: POST_BUILD State: SUCCEEDED
[Container] 2022/05/07 06:24:50 Phase context status code:  Message: 

リネームせよと言っている「demo-sls-person」はDynamoDBのテーブル名として設定した名前。あれ、でもこの直前に学んだ記事

初めてのサーバーレスアプリケーション開発 ~Serverless Framework を使ってAWSリソースをデプロイする~ | DevelopersIO

では、serverless.ymlなどのコードを修正してsls deployしなおしても特に何も言われなかったけどな……。何か違うのかしら。

こちらの記事を見ながら考えてみた。

CloudFormation内のDynamoDBリソース更新でハマった話 - Qiita

なるほど、わかってきたぞ。さっきserverless.ymlの修正した時、DynamoDBのパーティションキーが誤記っていることに気がついてスペルを直したんだった。それでそのままでは更新できないと言っているんですね。

こういう時はどうしたらいいのか……。他の記事も見ながら考えてみる。

AWS Cloudformation カスタム名つきリソースの置換 - 行けたら行く

ちゃんとやるなら、serverless.ymlに新しい名前のDynamoDBの定義を書いて一時的に2つある状態にして、データを移行させてから古い方を消すとかなのかな。でもそれだとそれやるたびに名前どんどん変わっていってしまう?

現状としてはDBに何もデータ入れていないし、今あんまり細かいところまで突っ込んで調べる段階でもないと思うので、 sls remove 相当のことをやって一旦スタックごと消してしまった方が早いのかな。ただ、今 sls deploy しているのはCodeBuildの中なわけで、Cloud9環境のかなから sls remove を打つのはなんかちょっと違うかなと思い、CloudFormationのサービス画面からスタックを削除できそうだったのでやってみた。

すると DELETE_FAILED になった。「状況の理由」には

The following resource(s) failed to delete: [ServerlessDeploymentBucket]. 

と表示されている。なんだろうこれは。リソースをよく見てみると「ServerlessDeploymentBucket」という論理IDでAWS::S3::Bucketタイプのものが削除に失敗していることがわかる。このServerlessDeploymentBucketという名前に見覚えはなかったし、これまでの手順でS3なんて使ってたっけ?と思ったが、S3の中身を見てみたらなんとなくわかった。デプロイ時にserverless.ymlとかの情報を一旦置いておく場所っぽい。

S3にある当該のバケットを直接削除し、その後CloudFormationに戻ってもう一度スタックの削除をしてみる。すると今後はこんなダイアログが表示された。もしかしてS3バケットを直接手で削除しなくてもこの手順踏めば勝手に削除してくれたのかもしれない。 ともあれ、これでスタックが削除することができた。この後もう一度git pushしてみるとCodeBuildによるデプロイは成功した。よかった。

全体の感想

なるほど、なんとなく雰囲気はわかった気がする。今後の課題としては、Lambdaのファンクションはどうやって何を書くべきのかはまだまだわかっていないのでその辺を勉強してみたい。あとせっかくなので連動して動くフロントエンドも作ってみたいところ。

MacにVoltaをインストールしてみる

目的

Dockerの勉強としてDocker公式ドキュメントの言語別ガイドの中のNode.jsを読んでいたところ、手順的にDocker上のNode.jsではなく素で動くNode.js環境が必要であるように見えた。

Overview | Docker Documentation

今振り返って見てみると別に必要ってほどではなかったけれど、とはいえMac上に直接存在するNode.js環境はあってもいい。

  • MacにNode.js環境作るぞ。
  • Node.jsのバージョン管理はできるようにしたいので、homebrew直じゃなくて管理ツールがあった方がいいよね。
  • 以前は nvmn を使っていたが、今調べてみると volta というのがあるらしい。

ということで、MacにVoltaをインストールして、Node.jsのバージョン違いの環境を手軽に手に入れられるようにするのが目的です。

Voltaって何と、Voltaのインストール

基本的にこちらを参考にして、そのままやりました。ありがとうございます。

zenn.dev

Node.jsの正確なバージョンをpackage.jsonに保存しているので、一度バージョンを固定するコマンドを打っておくだけでプロジェクトごとのバージョン管理がしやすい、というのがなんとなく良い点のような気がしました。

公式はこちら。

volta.sh

インストールが速いってことを推してますね。

では早速インストールしていきます。

% curl https://get.volta.sh | bash
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 12319  100 12319    0     0  10999      0  0:00:01  0:00:01 --:--:-- 10999
  Installing latest version of Volta (1.0.5)
    Checking for existing Volta installation
    Fetching archive for macOS, version 1.0.5
######################################################################## 100.0%#=#=-#  #                                                                   ######################################################################## 100.0%
    Creating directory layout
  Extracting Volta binaries and launchers
    Finished installation. Updating user profile settings.
Updating your Volta directory. This may take a few moments...
success: Setup complete. Open a new terminal to start using Volta!

.zshrcの中身を覗いて見て、イントールされていることを確認する。

% cat ~/.zshrc 
〜前略〜
export VOLTA_HOME="$HOME/.volta"
export PATH="$VOLTA_HOME/bin:$PATH"

一回Terminalを再起動して、voltaが実行できることを確認。そしてNode.jsがまだないことも確認してみる。

% volta --version
1.0.5
% node
Volta error: Node is not available.

To run any Node command, first set a default version using `volta install node`
Error details written to /Users/abcuser/.volta/log/volta-error-2022-03-16_17_58_33.674.log

この状態だと何もnodeがないので、デフォルトで使うNode.jsをVoltaでインストールしてみる。バージョン指定しないと最新のLTSリリースがインストールされるようです。

% volta install node
success: installed and set node@16.14.1 (with npm@8.5.0) as default
% node
Welcome to Node.js v16.14.1.
Type ".help" for more information.
> 
(To exit, press Ctrl+C again or Ctrl+D or type .exit)
> 

このときのinstallはめちゃめちゃ速く、4秒くらいしかかかっていなかった気がする。速いっていうだけある。

では次に、package.jsonがあるプロジェクトのディレクトリに移動して、そのプロジェクトのNode.jsのバージョンを固定してみます。固定するには volta pin というコマンドを使います。

% cd [package.jsonがあるプロジェクトのディレクトリ]
% volta pin node@12
success: pinned node@12.22.10 (with npm@6.14.16) in package.json

ここではバージョン12にしてみました。先ほど試しにinstallしたバージョンと違いますが、なかったら勝手にインストールもされるようです。この操作の後に package.json を覗いてみると、以下のようにnodeのバージョンが記入されていました。

  "volta": {
    "node": "12.22.10"
  },

基本的に使い方はこれだけでいいみたいです。簡単で便利ですね。

Dockerの勉強をしたときに参考した情報メモと、DevHubをDockerで動かす

目的

自分用のメモです。Dockerを勉強したときに参考のした情報の整理と、学んだ内容のちょっとしたメモです。学んだことを試すためにDevHubというOSSのツールをDockerで動かしてみたのでそれについても触れています。

環境

プライベートで使っているMacmacOS Catalina(10.15)。今流行のM1チップではありません。

インストール

Docker Desktop on Macをインストールした。現在はお仕事で使うと条件によっては有料になるようなので利用条件はきちんとご確認を。趣味で使う分には問題ないようです。

Install Docker Desktop on Mac | Docker Documentation

とりあえず基礎知識を身につける

最初に「Software Design 2021年12月号」の特集を読み始めたのだが、全く何も知らない状態で読むには難しい内容だった。 なので少しだけ読んで諦めて、Docker公式のGet Startedがあるのでそれに従って学ぶことにした。

docs.docker.com

上記のページにもTutorialが書いてあるが、手順に従って最初に動かすDockerコンテナ自身にも同じTutorialが記載されているのでどちらを見てもよい。

その後、たまたま見つけたこちらの記事を一通り読み、動かしてみる。第1回から第6回まである。

knowledge.sakura.ad.jp

ここまでで、おおなんとなく触れそうだなというところまでは来た。

個人的になるほどと思った点をまとめておく。

  • imageを作って、imageからcontainerを作る。
  • imageはベースとなるimage指定し、あとは自分の好きな差分を足せばお好みのimageを作り出せる。ベースとなるimageが世の中にいっぱい公開されているので、Node.js環境とかMongo DB環境とかすぐに手に入る。
  • imageを作るための設計書がDockerfile。Dockerfileを作って docker build コマンドでimageを作る。
  • docker run コマンドでimageからcontainerを作り出す。これでプロセスが起動した状態になる。
  • imageの作り方次第では複数のプロセスを1つのcontainerに入れることができそうだけど、Dockerの思想的には1つのcontainerには1つのプロセスとなるようにするのが基本らしい。
  • 複数のプロセスが連動して動くようなアプリケーションの場合それぞれを docker run しても良いが、Docker Composeを使うとまとめて docker run できる。
  • Docker Composeに使うファイルがdocker-compose.yml。作ったら起動や停止には docker-compose コマンドを使う。
  • containerは孤立した空間で動いているので、container同士はそのままでは通信できない。Docker独自のnetworkを定義してそれを通じて通信できるようにする必要があるが、Docker Composeを使うと割とそれが自然にできるところも便利。またその場合、docker-compose.ymlの services 配下に書いたサービス名がcontainer同士で通信するときにホスト名として使用できる(localhostって書いてもダメなので注意)。

もう少し実践的なことをやってみる(DevHubを動かす)

私、DevHubというツールのヘビーユーザーなので、DevHubをDockerで動かすことを目標にやってみます。DevHubはNode.jsとMongo DBで出来ているアプリケーションです。

github.com

Dockerfileを作ってみる。このファイルはDevHubをCloneしたフォルダの直下に置く。

FROM node:12

WORKDIR /usr/src/app

COPY package*.json ./
RUN npm install

COPY . .
RUN npm run build

EXPOSE 3000
CMD [ "node", "app.js", "-p", "3000", "-d", "devhub_db", "-t", "title" ]

通常DevHub環境構築するときはcloneした後に npm install だけで終わるが、docker build したときにはどうも package.json に書いてある postinstall が実行されていないように見えたので、postinstallに該当する npm run build を加えたらうまく行った。本当にpostinstallが実行されていなかったのかはきちんと検証できていないが、static/javascripts/devhub_bundle.js などが生成されていなかったのでそう判断した。 なぜ検証できていないのかというと、docker build実行中の標準出力にログとして表示されないから、より正確に言うとnpm installとかしたときのログは一時的に標準出力に表示されるが、完了するとパッと消えてしまうから。消えないようにする方法か、あるいは後からログを確認する方法があれば良いのだが、調べてみても結局わからなかった。残念(誰かわかる人がいたら教えて欲しい)。

さらに .dockerignore ファイルを作成する。これもDevHubをCloneしたフォルダの直下に置く。

node_modules
npm-debug.log

作ったDockerfileを使ってdocker buildする。ちなみに -t で指定するタグ名?は、小文字でないとダメらしい。

% docker build -t devhub .

無事にbuildできることを確認できたら、docker-compose.yml を作ってDevHubとMongo DB(とついでにMongo DBの管理ツールのMongo Express)が同時にcontainerとして動くようにする。このファイルもDevHubをCloneしたフォルダの直下に置く。

# Use root/example as user/password credentials
version: '3.1'

services:

  mongo:
    image: mongo:4
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: example
    # only if you'd like to persist the DB
    volumes:
      - ./db:/data/db
      - ./configdb:/data/configdb

  mongo-express:
    image: mongo-express
    restart: always
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: root
      ME_CONFIG_MONGODB_ADMINPASSWORD: example
      ME_CONFIG_MONGODB_URL: mongodb://root:example@mongo:27017/
    ports:
      - 8081:8081

  devhub:
    image: devhub
    working_dir: /usr/src/app
    environment:
      MONGODB_URI: mongodb://root:example@mongo:27017/devhub_db?authSource=admin
    ports:
      - 3000:3000

ここで起動する mongo は このdocker-composeで生成するcontainerからのアクセスのみ想定しているので ports は指定しなかった。もしcontainer外からもアクセスしたいのであれば 27017:27017 を指定すればよい。

mongoは起動時に何も指定しなければ特に認証なしでアクセス可能な状態で起動する。もしcontainer実行時にもそうしたければ、 mongoenvironment で指定している MONGO_INITDB_ROOT_USERNAMEMONGO_INITDB_ROOT_PASSWORD はいらない。その場合の MONGODB_URImongodb://mongo:27017/devhub とシンプルになる。

mongovolumes はデータベースを永続化したい(containerを停止/削除してまた開始したときにもデータベースの内容を引き継ぎたい)ときに指定する。 volumes で指定しているディレクトリは事前にディレクトリを作っておく必要はなく、勝手に作成される。

docker-compose.yml の準備が整ったら、docker-composeコマンドで起動する。

% docker-compose up -d

もろもろうまくいっていれば、 http://localhost:3000/ にブラウザでアクセスすればDevHubが動いていることが確認できる。

docker-compose up で起動したものは、 docker-compose down で停止する。停止と同時にcontainerの削除も行われる。

% docker-compose down

ここまで一区切り。ここまでのやり方だとdocker build時にテストを実行できていない(と思う)ので、それについてはまた別の記事で。

MacにpyenvでAnacondaをインストールしたときのバージョンメモ

目的

ほぼ皆様には特に何の得にもならない情報だと思いますが、MacでpyenvでAnacondaの各種バージョンを入れときに、そのまま conda で TensorFlowとKerasをインストールしたら、バージョンは何が入るのかをメモしておきます。

もちろん、自分でバージョンをちゃんと指定したりしてインストールすればなんかちゃんと自分の思い通りにできるのかとは思うんですが、そこまで理解が追いついていないのでただのメモです。

環境は macOS Catalina バージョン10.15.7、pyenvはhomebrewで入れた状態です。

インストール可能なAnacondaのバージョンの確認

以前Anacondaをインストールした時は、anaconda3-5.2.0 のようなバージョン名だったので、「5より新しいやつがあるのかな」みたいな気持ちでインストール可能なバージョンを確認してみた。

% pyenv install --list
(中略)
  anaconda3-4.4.0
  anaconda3-5.0.0
  anaconda3-5.0.1
  anaconda3-5.1.0
  anaconda3-5.2.0
  anaconda3-5.3.0
  anaconda3-5.3.1
  anaconda3-2018.12
  anaconda3-2019.03
  anaconda3-2019.07
  anaconda3-2019.10
  anaconda3-2020.02
  anaconda3-2020.07
(略)

うん? 5.3.1で止まっているように見える。 anaconda3-2020.07 みたいなバージョンとどういう関係性なんだろう?

Anacondaのリポジトリを見てみると、なんとなく分かった気がする。

repo.anaconda.com

Last Modifiedの日付を見る限り、前はanaconda3-5.3.0のような表記で、途中でバージョンの表記ルールが変わったということのようだ。日付が連続していて重複がないので。

ということは、anaconda3-2020.07 がこの時(2020年10月時点)の最新らしい。

anaconda3-2020.07をインストールしたとき

% pyenv install anaconda3-2020.07
Downloading Anaconda3-2020.07-MacOSX-x86_64.sh...
-> https://repo.continuum.io/archive/Anaconda3-2020.07-MacOSX-x86_64.sh
Installing Anaconda3-2020.07-MacOSX-x86_64...
Installed Anaconda3-2020.07-MacOSX-x86_64 to /Users/xxx/.pyenv/versions/anaconda3-2020.07
% pyenv rehash
% pyenv local anaconda3-2020.07

無事入った。なお、pyenvでanaconda(あるいは普通のpython)をインストールした後は 上記のようにrehashとlocalを打っているが、もちろんlocalじゃなくてglobalでもいい。自分の好みに合わせてください。rehashは打たないとインストールしたpythonが使えなかったので必要っぽい雰囲気。

anaconda3-2020.07によってインストールされたpythonは以下のようにバージョンは3.8.3のようだ。

% python --version
Python 3.8.3

ではtensorflowとkerasをcondaでインストールしてみる。まずはtensorflowから。

% conda install tensorflow
Collecting package metadata (current_repodata.json): done
Solving environment: failed with initial frozen solve. Retrying with flexible solve.
Solving environment: failed with repodata from current_repodata.json, will retry with next repodata source.
Collecting package metadata (repodata.json): done
Solving environment: failed with initial frozen solve. Retrying with flexible solve.
Solving environment: - 
Found conflicts! Looking for incompatible packages.
This can take several minutes.  Press CTRL-C to abort.
failed                                                                                                                                         

UnsatisfiableError: The following specifications were found
to be incompatible with the existing python installation in your environment:

Specifications:

  - tensorflow -> python[version='2.7.*|3.7.*|3.6.*|3.5.*']

Your python: python=3.8

If python is on the left-most side of the chain, that's the version you've asked for.
When python appears to the right, that indicates that the thing on the left is somehow
not available for the python version you are constrained to. Note that conda will not
change your python version to a different minor version unless you explicitly specify
that.

どうもバージョンが合わないって言われているようだ。回避方法とかあるのかもしれないが、深く追うつもりはなかったのでさっさと諦めてもう少し古いバージョンのAnacondaを入れてみることにする。

anaconda3-2020.02をインストールしたとき

さっきのメッセージによると3.7.xなら良いようなので、3.7.xが入るanacondaを探すと 2020.02 ならそうっぽい。ということで入れてみる。

% pyenv install anaconda3-2020.02
Downloading Anaconda3-2020.02-MacOSX-x86_64.sh...
-> https://repo.continuum.io/archive/Anaconda3-2020.02-MacOSX-x86_64.sh
Installing Anaconda3-2020.02-MacOSX-x86_64...
Installed Anaconda3-2020.02-MacOSX-x86_64 to /Users/xxx/.pyenv/versions/anaconda3-2020.02
% pyenv rehash
% pyenv local anaconda3-2020.02

入った。ではtensorflowとkerasをインストールしてみる。

% conda install tensorflow
% conda install keras

今度はエラーにならずに両方入った。コマンドライン上で各バージョンを確認してみる。

% python
Python 3.7.6 (default, Jan  8 2020, 13:42:34) 
[Clang 4.0.1 (tags/RELEASE_401/final)] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import keras
Using TensorFlow backend.
>>> print(keras.__version__)
2.3.1
>>> import tensorflow as tf
>>> print(tf.__version__)
2.0.0

うん、良さそう。が、TensoFlowが2.0.0になっているのが気になった。 TensorFlowは1.xと2.xで書き方が結構違うという情報があったので(実際既に用意していたスクリプトはエラーになった。placeholderとか使えないらしい)、pyenvで環境を用意している利点を生かして、TensorFlow 1.xがインストールされている環境も作っておこう。

anaconda3-2019.07をインストールしたとき

conda install tensorflow のときにバージョン指定すれば任意のバージョンが入れられそうではあるが、横着で単にAnacondaのバージョンをさらに古くする作戦に出る。

さらに古いやつということで、anaconda3-2019.10をインストールして conda install tensorflow してみたが、入ったTensorFlowは2.0.0だった。

ということでさらに古いanaconda3-2019.07を入れてみる。

% pyenv install anaconda3-2019.07
Downloading Anaconda3-2019.07-MacOSX-x86_64.sh...
-> https://repo.continuum.io/archive/Anaconda3-2019.07-MacOSX-x86_64.sh
Installing Anaconda3-2019.07-MacOSX-x86_64...
Installed Anaconda3-2019.07-MacOSX-x86_64 to /Users/xxx/.pyenv/versions/anaconda3-2019.07
% pyenv rehash
% pyenv local anaconda3-2019.07
% conda install tensorflow
% conda install keras
% python
Python 3.7.3 (default, Mar 27 2019, 16:54:48) 
[Clang 4.0.1 (tags/RELEASE_401/final)] :: Anaconda, Inc. on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import keras
Using TensorFlow backend.
>>> print(keras.__version__)
2.3.1
>>> import tensorflow as tf
>>> print(tf.__version__)
1.14.0

お望みどおり、1.xが入った。

まあ繰り返しますが、ちゃんと自分で好みのバージョンを指定してインストールすればいいだけのような気がしますが、調べるのが面倒だったので、Anacondaのバージョンを変えるという横着なことをしたときの記録でした。

macにhomebrewのインストールする前の/usr/local下の状態

メモとして残したいのは今更homebrewのインストールの仕方ではなくて。 homebrewをインストールする前の /usr/local の下がどうなっていたのかをメモしたいだけ。

HDDまっさらな状態から macOS Catalina (10.15.7) をインストールしたばかりの状態(正確にはそこからXcodeApp Storeからインストールした状態)では、/usr/local の下にはファイルもフォルダも何一つ存在しなかった。

homebrewをインストールしようとすると以下のように表示されるので、今後 /usr/local 下にあるものは基本的にhomebrewによってインストールされたものって考えて良さそう。

==> This script will install:
/usr/local/bin/brew
/usr/local/share/doc/homebrew
/usr/local/share/man/man1/brew.1
/usr/local/share/zsh/site-functions/_brew
/usr/local/etc/bash_completion.d/brew
/usr/local/Homebrew
==> The following new directories will be created:
/usr/local/bin
/usr/local/etc
/usr/local/include
/usr/local/lib
/usr/local/sbin
/usr/local/share
/usr/local/var
/usr/local/opt
/usr/local/share/zsh
/usr/local/share/zsh/site-functions
/usr/local/var/homebrew
/usr/local/var/homebrew/linked
/usr/local/Cellar
/usr/local/Caskroom
/usr/local/Homebrew
/usr/local/Frameworks
==> The Xcode Command Line Tools will be installed.

HerokuとMongoDB AtlasでDevHubを動作させる

目的

私、DevHubというコミュニケーションツールを長年愛用しています。今ではこれなしでは何もできません。おそらく世界一のヘビーユーザだと自負しています。

github.com

このDevHub、Node.js+MongoDBで動いてまして、とりあえずクラウド上にデプロイして使ってみたいなという場合は、HerokuにデプロイしてMongoDBはHerokuのAdd-onであるmLabを使う、というのが選択肢の一つでした。

しかし、2020年11月10日でmLabはHeroku Add-onの提供を終了するとアナウンスがありました。

Shutdown of mLab's Heroku Add-on on November 10, 2020 | mLab Documentation & Support

DevHubをHeroku上で動作させる場合、このmLabというHeroku Add-onは大変使い勝手が良く、Herokuの管理画面上でぽちぽちっと少しやるだけですぐMongoDBが使えるようになる大変ありがたい存在でした。しかしこれが使えなくなる……これは事件です。

mLabの代わりとして、Herokuは別のAdd-onを紹介していますが値段が結構高く、無料プランもありません。一方、mLab自体はMongo DB AtlasというMongoDB as a Serviceに移行することを勧めています。

MongoDB AtlasはHerokuとはまったく別のサービスなので当然Herokuとは別にアカウントを作る必要があり、Heroku Add-onと比べると運用上の手間は増えます。しかしMongoDB AtlasはmLabとほぼ同じようなプランを取り揃えいて値段もむしろ安くなっています。なんといっても無料プランもちゃんとある!

ということで本記事では、DevHubのソースコードはHerokuにデプロイしてHeroku上で動作させ、MongoDBは別サービスであるMongoDB Atlasを使う、というのをやってみます。新規にDevHubを構築する例とします。すでにHeroku上で動いているものがある場合は前述のサイトに移行(Migration)手順の紹介がありますのでそちらを参考にしてください。

注意

DevHubはチャット+共有メモが使える大変使い勝手の良いツールですが、セキュリティやユーザ管理には重きを置いていない設計なので基本的に社内とかの閉じた環境で使う前提のツールです。クラウド上で使用するのはあくまで自己責任でお願いします。

一応Basic認証もありますので、機密性がさほど高くないデータを扱うのであればクラウド上に配置するのもありですが、会社で使うのであればクラウド上へのデプロイはやめておいた方がいいです。大人しくSlackとか使いましょう。

全体の流れ

  • Herokuで新規アプリケーションを作成し、GitHubにあるDevHubのリポジトリを関連づけてデプロイする。
  • Herokuの管理画面で、環境変数を追加する。
  • MongoDB AtlasでClusterを新規作成する。
  • 作成したClusterに対する接続文字列を手に入れ、Herokuの管理画面で環境変数として設定する。
  • うまくいけばこれでDevHubが使えるようになる。

HerokuでDevHubをデプロイする

Herokuで新規アプリケーションを作成する

  • Herokuのアカウントをまだ作成していない場合は作成する。有料プランを使うつもりがないならクレジットカードの登録はしなくても大丈夫。
  • Herokuのダッシュボードで、Node.jsの新規アプリケーションを作成する。名称(URLにも使われる)と、Regeionを選ぶだけ。RegionはUnited StatesとEuropeが選べたが、デフォルトでUnited Statesが選択されていたのでそれにした。

DevHubのリポジトリを関連づけてデプロイ

私の場合、volpe28v/DevHub をFolkしていたので自分のリポジトリを関連づけた形になりますが、Folkしていない場合はHeroku CLIを使ったやり方が必要かもしれません(後述)。

  • 作ったアプリケーションの"Deploy"画面に自動的に遷移してくるので、"Deployment method"のところで"GitHub"をクリック。

    f:id:hachiilcane:20200817003837p:plain
    HerokuでDeployment methodを選択

  • GitHubの認証画面らしきものが表示されるので、紐付けたいアカウントで認証を通す(自分の場合はすでにログイン済みだったのでいきなり自分のGitHubアカウントが表示された)。

  • うまくいくと"Deploy"画面に"Connect to GitHub"が表示されているので、使用したいリポジトリをSearchして"Connect"ボタンをクリック。
  • リポジトリがConnectedになると、"Deploy"画面上に"Automatic deploys"と"Manual deploy"が表示される。どちらでも好きな方を使える。どちらの場合も使いたいブランチ名をプルダウンから選んでボタンをクリックするだけ。ブランチ名を選べるので、masterブランチじゃないブランチをデプロイしたい場合にも対応できそう。

    f:id:hachiilcane:20200817004505p:plain
    HerokuのAutomatic deploysとManual deploy

  • 今回は"Manual deploy"でブランチを選択し、"Deploy Branch"ボタンをクリック。

  • GitHubからソースを持ってきてBuildが始まる。Buildのログはブラウザ上でリアルタイムに表示されるので安心。何も問題なかったらこれでDeployが完了する。
  • この時点ではまだアプリケーションを開かないほうがいい。

GitHubリポジトリと直接連携する以外の方法

なお、今回はGitHubリポジトリと直接連携する方法を選びましたが、GitHubのDevHubをCloneして、手元にあるソースコードをHerokuにデプロイすることも可能です。その場合、"Deployment Method"で"Heroku Git"を選べば、やり方が表示されます。大まかにはHeroku CLIというコマンドラインツールをインストールして、コマンドラインからherokuにpushするやり方です。さほど難しくはないので、マニュアルをよく読んで対応しましょう。

DevHubで使用するいくつかの環境変数を設定する

  • アプリケーションの画面を開く前に、"Settings"の画面に行き、環境変数の設定をする。"Reveal Config Vars"ボタンをクリックすると設定できる。以下のKEY/VALUEを設定する。
KEY VALUE 説明
BASIC_AUTH_USER 任意のユーザ名 これを設定するとBasic認証が使えるようになります
BASIC_AUTH_PASS 任意のパスワード 同上
FORCE_SSL true httpsを強要できます
GRIDFS true アップロードファイルがDB内に格納されるようになります。falseの場合、デプロイされたOS上のファイルシステムに直接ファイルとして保存されます。
TZ タイムゾーン(Asia/Tokyoとか) いらないかもしれない

MongoDB Atlasのアカウントを作り、Clusterを新規作成する

アカウント作成

  • MongoDB Atlasのサイトに行き、"Start Free"のボタンをクリックしてアカウントを作成する。言われる通りに進めれば作れる。作ると早速Clusterを作るように言われるが、いったんキャンセルしておく。有料プランを使うつもりがないなら、クレジットカードの登録はなくて大丈夫。 Managed MongoDB Hosting | Database-as-a-Service | MongoDB

Organization / Project / Cluster という概念を理解しておく

MongoDB Atlasは、Organization / Project / Cluster という階層構造になっているので、まずはこの用語の意味を押さえておく。こちらの記事が大変参考になりました。

MongoDB Atlasを使い始める (MongoDB as a Service) - Qiita

データベースアクセス用のユーザを作成する

前述の記事にも書いてあるように、MongoDBにアクセスするにはAtlasのユーザとは別にMongoDBアクセス用のユーザが必要になります。このユーザはProjectに紐づきます。Clusterを作成してからでもいいです。

  • "Database Access"からデータベースアクセス用のユーザを一つ作成した。ロールは"atlasAdmin@admin"という一番強そうなやつを選んだ。

Clusterを新規作成する

  • Project名はデフォルトでは"Project 0"とかになっているので、変えたければ変えておく(後からでも変えられる)。
  • Projectを選び、"Clusters"からClusterを新規作成する。"Build a cluster"ボタンがあったのでそれをクリック。
  • "Shared Clusters", "Dedicated Clusters", "Dedicated Multi-Region clusters"の3つから選ぶ。後ろ2つは有料なので、"Shared Clusters"を選ぶ。これはFREE。
  • 次の画面で細かな設定やプランを選択する。"Cloud Provider & Region"で好きなプロバイダーとリージョンを選ぶ。よくわからないのでデフォルトのままにしておいた(AWS, us-east-1)。"Cluster Tier"は"M0 Sandbox (Shared RAM, 512MB Storage)"、"Additional Settings"は"MongoDB 4.2, No Backup"、"Cluster Name"で好みのCluster名をつける。Cluster名はあとで変更することはできないのでこのタイミングでちゃんと考えてつける。
    f:id:hachiilcane:20200817004756p:plain
    MongoDB Atlasでの Cloud Provider & Region の選択
    f:id:hachiilcane:20200817004855p:plain
    MongoDB Atlasでの Cluster Tier などの選択
  • Clusterが作成されたのを確認して(数分かかる)、"Network Access"から"IP Whitelist"の設定をする。"+ADD IP ADDRESS"ボタンをクリックし、"ALLOW ACCESS FROM ANYWHERE"ボタンをクリックすると"Whitelist Entry"に"0.0.0.0/0"が入力されるのでこれで"Confirm"ボタン。HerokuはIPアドレスが固定ではないという話があったので(公式情報は未確認だが)こうしておいた。

接続文字列を手に入れ、Herokuの管理画面で環境変数として設定する

MongoDB Atlasの画面で、Clusterに対する接続文字列を手に入れる

  • "Clusters"からClusterを表示すると、"CONNECT"ボタンがあるのでクリック。すると"Choose a connection method"という画面が表示されるので、"Connect your application"を選択。
  • "Select your driver and version"でDriverとして"Node.js"、Versionとして"3.6 or later"を選択。すると、"Add your connection string into your application code"のところに接続文字列が表示されるのでコピーしておく、ユーザ名は事前に作成しておいたMongoDBユーザの名前がすでに入っている。<password>はユーザのパスワードで置き換え、<dbname>のところは"the name of the database that connections will use by default"と説明があるのでそれで置き換える。たぶん実際はなんでもいいんだろうけど、とりあえずDevHubのデフォルトDB名である"devhub_db"にしておく。

Heroku側で環境変数を追加

  • Herokuの"Settings"の画面に戻り、環境変数の追加をする。"MONGODB_URI"というKEY名にVALUEとしてさっき手に入れたMongoDBの接続文字列を入れる。
KEY VALUE 説明
MONGODB_URI 先ほど手に入れた"mongodb+srv://"で始まる接続文字列

DevHubが動くかどうか確認

Herokuの管理画面に"Open app"というボタンがあるのでそれをクリックするとデプロイしたアプリケーションの画面に飛べる。

ここまでの作業がちゃんとできていれば、ユーザ名を入れてすぐに使える状態になっている。

画面は表示されるけど歯車のマークがずっとグルグル回っている状態になっている場合は、データベースに関する設定がうまくいっていないことが多いので見直してみてください。

Let's EncryptでPostfix+Dovecotにちゃんとした証明書を設定する(Ubuntu 18.04)

目的

さくらのVPSUbuntu 18.04 LTS(Ubuntu 16.04 からアップグレードした)上でPostfix+Dovecotのメールサーバを運用しています(運用といっても、使っているのは自分だけ)。SSL/TLS対応はすでにしてありますが、証明書はオレオレ証明書を使っていました。

オレオレ証明書でも長いこと大丈夫でしたが、昨今のセキュリティ事情を踏まえて、ちゃんとした証明書にしたいところです。Let's Encryptを使うと無料で証明書を作成できるようですので、さっそく憧れのちゃんとした証明書を手に入れてメールサーバに設置したいと思います。

ということで、やってみた内容をメモしておきます。

注意点として、私のサーバはWebは運用していない(動かしてもいない)ので、あくまでPostfix+Dovecotに証明書を設置することしか考えていない内容になっています。Webサービスを運用している人の場合はまた違ったやり方があるんじゃないかと思います。

また、ここでは Postfix+Dovecotのインストール、DNSの設定、およびSSL/TLS対応はすでにできていることを前提としています。オレオレ証明書をLet's Encryptで取得した証明書に差し替えることが目的です。

はじめに:大まかな流れ

やり始めたら色々気になったことを調べまくってしまって、その過程も書いてあるのでやたら記事が長いです。最初に謝っておきます。

だけど結果的にはこんな感じだけで済みます。

  • certbotの公式サイトのUbuntu 18.04 LTS用の手順に従い、certbotをインストールする。
  • certbotで証明書を作成する。
  • 使用するサービスの設定ファイルの証明書と鍵のパスを作ったものに合わせる。
  • サービスをreloadして動作確認。
  • 証明書更新の仕組みはcertbotインストール時にできてしまっている。あとはcertbotのhook機能を使って証明書更新後にサービスをreloadする処理を入れるだけでOK。

Let's Encryptすごい。わかってしまえばすごく簡単です。

Let's Encryptの公式を確認する

まず、Let's Encryptの公式はここみたいです。

letsencrypt.org

「はじめに」とかいろいろドキュメントを読み込んでいく。証明書の自動更新をするには普通ACMEクライアントを使うようだが、ここの記述によると、公式としては Certbot というクライアントをおすすめしている。

ACME クライアント実装 - Let's Encrypt - フリーな SSL/TLS 証明書

おすすめしているので、Certbotを使うことにする。

certbot をインストールする

Ubuntuの場合、Certbotはaptで入れられるのかしらと思ってググって調べてみると、どうも人によって結構やり方が違う。普通にapt installしている人もいれば、Ubuntuの標準リポジトリに入っているcertbotは若干古いからという理由でリポジトリを追加してからやっている人もいるし、はたまたGitHubからcloneしている人もいる。

うーん、どうするか。ひとまず公式に近い情報を知りたい。Let's Encrypt公式にはACMEクライアントの紹介をしているが具体的なやり方はある意味ほとんど書いていない。ので、上記のサイトの Certbot というリンクから、Certbotの公式の情報を調べてみる。

すると certbot の公式と思われるサイトに飛ぶわけだが、なんだろう、なんかすごく詐欺サイトっぽい雰囲気なんだが……。いやまあでもこれが公式らしいです。

certbot.eff.org

このサイトがまたすごく見づらいというか調べづらくて、必要な情報がどこにあるのかわかんない。

My HTTP website is running [Software] on [System]

となっていて、プルダウンで自分の状況に合わせた選択肢を選ぶと説明が出てくる作りになっているっぽいが、いや私はWebサイト用意したいんじゃない、Nginxとかそんなんじゃないねん……とひとしきり悩んだ挙句、Softwareのところを「None of the above」、Systemのところを「Ubuntu 18.04 LTS (bionic)」を選べば良いということに気づいた。

するとちゃんとcertbotを使った証明書更新のやり方とか全部出てきた。これですよ、探していたのは。それによると、Ubuntu 18.04 LTSの場合のcertbotのインストール手順は以下のような感じ。

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository universe
$ sudo add-apt-repository ppa:certbot/certbot
$ sudo apt-get update

$ sudo apt-get install certbot

Ubuntu 18.04とUbuntu 20.04ではどう手順が違うか

話が脱線してしまいますが、Ubuntu 20.04の手順とはどう違うのか気になってしまったので、前述のサイトで表示された内容のdiffをとってみた。

すると違うのは certbot のインストール方法、正確にはcertbotをaptでインストールする前の、リポジトリの登録の仕方が違うだけだった。

Ubuntu 20.04の場合の手順はこんな感じだった。

$ sudo apt-get update
$ sudo apt-get install software-properties-common
$ sudo add-apt-repository universe
$ sudo apt-get update

$ sudo apt-get install certbot

そもそもこのインストール手順は何をしているのか

Ubuntu 18.04の場合との差分を見ながら、そもそもここで行っていることはなんなのかを勉強を兼ねて調べてみる。

まず add-apt-repository universe とあるが、これは apt でインストールするときに使われるリポジトリとして universe を追加することを意味する。

universeというのは、Ubuntuで使われるリポジトリの一つで、 main/restricted/universe/multiverse の4つがあるらしい。今どれを使う設定になっているかは、 /etc/apt/sources.list というファイルの中身を見るとわかるらしい。

Ubuntuのパッケージ管理のメモ - 蒼の王座・裏口

そして apt-get install software-properties-common でいれている software-properties-commonだが、これは add-apt-repositoryコマンドを使うのに必要なパッケージらしい。

Ubuntu Linuxで、add-apt-repositoryしようとして「コマンドがない」って言われたら - CLOVER🍀

そして Ubuntu 18.04の場合のみにある add-apt-repository ppa:certbot/certbot だが、これはPPA(Personal Package Archive)をリポジトリとして追加するということらしい。これは全然知らなかった。こちらの記事が大変参考になりました。

kazuhira-r.hatenablog.com

まとめると、Ubuntu 18.04向けとしてはcertbotをaptでインストールするためにはcertbot/certbotというPPAをリポジトリに追加する必要があったが、Ubuntu 20.04向けとしては必要なものはすべてuniverseリポジトリに入っているのでPPAの追加は必要なくなった、という感じか(推測入っているので正確ではない可能性あり)。

で結局自分はどうcertbotをインストールしたか

なるほど、少し理解が進んだところで自分の環境を確認してみる。 /etc/apt/sources.list を見てみると、universeリポジトリはすでに登録されている。add-apt-repositoryコマンドはなく、software-properties-commonは入っていない。ということで、最終的に以下の手順で certbot をインストールした。

$ sudo apt update
$ sudo apt install software-properties-common
$ sudo apt-add-repository ppa:certbot/certbot
$ sudo apt update

$ sudo apt install certbot

なお、 sudo apt-add-repository ppa:certbot/certbot を実行した時点で、 /etc/apt/sources.list.d/ の下に、 certbot-ubuntu-certbot-bionic.list というファイルが追加されていた。

certbotで証明書を作成する

ファイアウォールの80番ポートを開ける

チャレンジのタイプ - Let's Encrypt - フリーな SSL/TLS 証明書

上記のLet's Encryptの公式にもあるように、

Let’s Encrypt から証明書を取得するときには、ACME 標準で定義されている「チャレンジ」を使用して、証明書が証明しようとしているドメイン名があなたの制御下にあることを検証します。

となっていて、チャレンジは複数のやり方から選択できる。おすすめは「HTTP-01 チャレンジ」らしい。最初は「DNS-01 チャレンジ」を使おうかとなんとなく考えていたがリスクもあるらしく、「HTTP-01 チャレンジ」が最も多く使われているとのこと。おすすめに従っておこう。

「HTTP-01 チャレンジ」に対応するため、ファイアウォールのHTTPのポートを開ける。iptablesを使っているので、iptablesで80番を開ける。 ここでのやり方はさくらのVPSでUbuntu16.04をインストールした環境でのやり方なので、他の環境ではまたちょっと違うはずなのでご注意を。ちなみに7とは7行目に入れるの意味。

$ sudo iptables -L --line-numbers
$ sudo iptables -I INPUT 7 -p tcp -m tcp --dport 80 -j ACCEPT
$ sudo iptables -L --line-numbers
Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination         
1    DROP       tcp  --  anywhere             anywhere             tcp flags:FIN,SYN,RST,PSH,ACK,URG/FIN,SYN,RST,PSH,ACK,URG
2    DROP       tcp  --  anywhere             anywhere             tcp flags:!FIN,SYN,RST,ACK/SYN state NEW
3    DROP       tcp  --  anywhere             anywhere             tcp flags:FIN,SYN,RST,PSH,ACK,URG/NONE
4    ACCEPT     all  --  anywhere             anywhere             state RELATED,ESTABLISHED
5    ACCEPT     icmp --  anywhere             anywhere            
6    ACCEPT     all  --  anywhere             anywhere            
7    ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:http
8    ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:smtp
9    ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:urd
10   ACCEPT     tcp  --  anywhere             anywhere             tcp dpt:imaps
11   ACCEPT     tcp  --  anywhere             anywhere             state NEW tcp dpt:xxxx
12   REJECT     all  --  anywhere             anywhere             reject-with icmp-host-prohibited

Chain FORWARD (policy ACCEPT)
num  target     prot opt source               destination         
1    REJECT     all  --  anywhere             anywhere             reject-with icmp-host-prohibited

Chain OUTPUT (policy ACCEPT)
num  target     prot opt source               destination   

$ sudo iptables-save | sudo tee /etc/iptables/iptables.rules

それにしてもほんと、iptablesって難しいな。ufwに移行したい。

certbotでコマンドを打って証明書作成

--help allで使い方表示してくれるらしい。

$ sudo certbot --help all

では certonly --standalonecertbotを実行する。対話形式で進むので、-d オプションとかなくても大丈夫。

$ sudo certbot certonly --standalone
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): xxx@yyy.zzz

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N
Please enter in your domain name(s) (comma and/or space separated)  (Enter 'c'
to cancel): mail.xxx.zzz
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for mail.xxx.zzz
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/mail.xxx.zzz/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/mail.xxx.zzz/privkey.pem
   Your cert will expire on 2020-08-02. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - Your account credentials have been saved in your Certbot
   configuration directory at /etc/letsencrypt. You should make a
   secure backup of this folder now. This configuration directory will
   also contain certificates and private keys obtained by Certbot so
   making regular backups of this folder is ideal.
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

なんかあっさりできあがった。途中で入力したのは、Yes/No系のやつを除けば、更新時期が近づいたらメールを送ってくれるらしいのでその連絡用メールアドレスと、メールサーバのFQDN(mail.xxx.zzzみたいなやつ)だけ。できたものは /etc/letsencrypt/live/mail.xxx.zzz/ に置かれている。

これまでオレオレ証明書を散々作ってきたので、あっさり作成できすぎてむしろ気味が悪い。えっ、CN以外の情報、Country NameとかOrganization Nameとかはどうなったんだ? すごく気になるので、opensslコマンドを使って出来上がった fullchain.pem の内容を確認してみる。

$ sudo openssl x509 -text -noout -in /etc/letsencrypt/live/mail.xxx.zzz/fullchain.pem

すると、Subjectの情報としてはCNしか書かれていない。CもOもない。えっ、他の情報は別になくてもよかったのか……? なんということだ……(衝撃)。

気を取り直して X509v3 extentionsの様子も見てみる。するとSAN(Subject Alternative Name)も含めて、いい感じに情報がセットされている。X509v3 extentionsは時代とともに必要な情報が変わっていくから、この辺を全部いい感じにしてくれるのはすごくありがたい。

PostfixDovecotの証明書のパスを書き換える

/etc/postfix/main.cf の中に書いてある証明書と鍵のパスを変更する。

smtpd_tls_cert_file=/etc/letsencrypt/live/mail.xxx.zzz/fullchain.pem
smtpd_tls_key_file=/etc/letsencrypt/live/mail.xxx.zzz/privkey.pem

/etc/dovecot/conf.d/10-ssl.conf の中に書いてある証明書と鍵のパスを変更する。

ssl_cert = </etc/letsencrypt/live/mail.xxx.zzz/fullchain.pem
ssl_key = </etc/letsencrypt/live/mail.xxx.zzz/privkey.pem

できたらサービス再起動。

$ sudo systemctl restart postfix.service
$ sudo systemctl restart dovecot.service

エラーは出ていない。大丈夫のようだ。

と、最初は上記の通り restart を使ったが、後から動作確認してみたところ、reloadで証明書パスの変更は認識して動いていた。なので、以下のように reload でよくて、かつ複数サービス名を並べても動いてくれるみたい。

$ sudo systemctl reload postfix.service dovecot.service

証明書が本当に変わっているか接続確認

WebサーバならばHTTPSで接続してブラウザから証明書の確認すればすぐだけど、メールサーバだとどうやって接続確認すればいいのか。Thunderbirdでメール送受信できているので問題はなさそうだが、証明書がどうなっているのかを表示する方法がわからなかった。

調べてみると、openssl s_client を使う方法があるのですね。なるほど。

Postfixに入れたSSL証明書の更新手順 - Qiita

$ openssl s_client -connect mail.xxx.zzz:465 -showcerts
$ openssl s_client -connect mail.xxx.zzz:993 -showcerts

おおっ、ちゃんと変わっている。VerifyもOKになっている。オレオレじゃない。やったー!

certbotが自動的に更新処理を行うようにする

自動更新のコマンドを確認

certbotによる証明書作成時にも出力されていたように、更新処理は certbot renew を実行すればよいだけ。ちゃんと実行できるか確認したい場合は $ sudo certbot renew --dry-run とすれば試し実行してくれる。

この certbot renew の処理が大変賢くて、文字通り打つだけで必要なことを全部自動的にやってくれる。 certbot certonly で証明書作成したときの情報を /etc/letsencrypt/ 下にちゃんと残しておいてあるらしく、

  • 作成したすべての証明書をチェックし、有効期限が30日未満になっているものだけを更新処理してくれる。
  • 更新された証明書は、 最初に置かれた /etc/letsencrypt/live/mail.xxx.zzz/ に同じファイル名で置かれる(ので設定ファイルから見たときにパスが変わらない)。古くなった証明書は自動的に /etc/letsencrypt/archive/mail.xxx.zzz/ に格納されていく(実体はむしろarchive側にあって、liveのほうはシンボリックリンクになっている様子)。

といったことを自動で行ってくれる。すごい。ほんとすごい。

あとは certbot renew の実行を定期的かつ自動的に行うようにすればよい。

certbot renew の自動実行の登録……はすでに行われていた

実はこの後、自動実行のやり方をさんざん時間かけて調べて、cronで行う方法を確立できるところまで調べた。

が、結論から言うと、certbot renewの自動実行はすでに systemdのタイマーを使ったやり方で設定されていた。これは certbot をインストールしたときに自動的にやってくれていたみたいで、certbot の公式サイトに書いてあったUbuntu 18.04用のやり方に従ってインストールしたおかげだろう。まさかそんなことまでやってくれているなんで思ってもみなかった。

確かに、前述したcertbotUbuntu 18.04用の手順にはこう書いてあった。

The command to renew certbot is installed in one of the following locations:

/etc/crontab/
/etc/cron.*/*
systemctl list-timers

これ、最初読み違えていて、「このうちのどれかでやるといいよ」って言っていると思ってた。よく見たら「インストールされるよ」って書いてあるじゃん……。2日間くらいかけていろいろ調べてたわ、私。アホすぎる。

なので、certbot renewの自動実行はすでに行われるようになっている。なっているのだが、一応どうなっているのかを確認しておく。

systemdに登録された certbot.timer の内容を確認してみる

systemdのタイマーは初めて扱うが、cronの代わりとして使えるものらしい。 systemctl list-timers で起動しているタイマーを表示できるらしいのでまずは状況確認(これはsudoいらなかった)。

$ systemctl list-timers
NEXT                         LEFT          LAST                         PASSED       UNIT                 
Wed 2020-05-06 14:00:28 JST  32min left    Wed 2020-05-06 13:03:23 JST  24min ago    anacron.timer        
Wed 2020-05-06 19:26:58 JST  5h 58min left Wed 2020-05-06 02:16:23 JST  11h ago      certbot.timer        
Wed 2020-05-06 20:28:33 JST  7h left       Tue 2020-05-05 20:28:33 JST  16h ago      systemd-tmpfiles-clea
Wed 2020-05-06 23:54:30 JST  10h left      Wed 2020-05-06 08:51:33 JST  4h 36min ago apt-daily.timer      
Thu 2020-05-07 06:56:11 JST  17h left      Wed 2020-05-06 06:14:23 JST  7h ago       apt-daily-upgrade.tim
Thu 2020-05-07 10:59:51 JST  21h left      Wed 2020-05-06 12:39:23 JST  48min ago    motd-news.timer      
Mon 2020-05-11 00:00:00 JST  4 days left   Mon 2020-05-04 00:00:33 JST  2 days ago   fstrim.timer         

7 timers listed.
Pass --all to see loaded but inactive timers, too.

ややっ、確かに certbot.timer なるものがいる。しかも稼働している。いつのまに。 ユニット設定ファイル(?)の実体を探してみる。

$ ls -al /lib/systemd/system | grep certbot
-rw-r--r--  1 root root   233 Feb 10  2019 certbot.service
-rw-r--r--  1 root root   156 Feb 10  2019 certbot.timer

おお、確かにある。certbotをインストールしたときにできたようだ。サービスユニットとタイマーユニットのセットだ。

/lib/systemd/system/certbot.timerの中身。

[Unit]
Description=Run certbot twice daily

[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=43200
Persistent=true

[Install]
WantedBy=timers.target

/lib/systemd/system/certbot.serviceの中身。

[Unit]
Description=Certbot
Documentation=file:///usr/share/doc/python-certbot-doc/html/index.html
Documentation=https://letsencrypt.readthedocs.io/en/latest/
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot -q renew
PrivateTmp=true

certbot.timerのほうで、毎日0時と12時に実行される設定になっている。実行されるのは名前が対応している certbot.service で、実際に実行されるのは /usr/bin/certbot -q renew であることがわかる。

ほうほう。なるほど、確かにもう定期的にcertbot renewする仕組みが出来上がってますね。1日2回もやらなくても週に1回くらいでいい気がしますが。

これで完了! ……と思いきやあと一つだけ。

certbot renewで証明書の更新に成功したあと、サービスをreloadするようにする

これもさんざん調べて、長くなるので詳細は後述するが、結論としてはcertbot renewで証明書の更新に成功した後に postfixdovecot を reload しないと新しい証明書が反映されない。

certbotにはhook機構が用意されているのでこれを使う。certbotの公式ドキュメント、それと前述したUbuntu 18.04用のcertbot手順が参考になる。

https://certbot.eff.org/docs/using.html?highlight=hooks#renewing-certificates

/etc/letsencrypt/renewal-hooks/deploy/ に、以下の内容をスクリプトを作成して置く。名前は仮に reloadmail.sh とした。

#!/bin/sh
systemctl reload postfix.service dovecot.service

そして作成したファイルに実行権限をつける。

$ sudo chmod 755 reloadmail.sh

これで certbot renew で更新されたときにはサービスをreloadしてくれるはず。が、実際に証明書が更新されるのは約60日後なので、それまで動作検証ができない。もし間違ってたらごめんなさい。

なお、Hookスクリプトを用意するやり方ではなく、 certbot renew --deploy-hook "systemctl reload postfix.service dovecot.service" のように certbot renew に --deply-hookオプションをつけるやり方もある。

さらに蛇足だが、certbot renew && systemctl reload postfix.service dovecot.service のような && を使った記述でcronなどに登録するやり方も考えられる。しかしこれはcertbot renew実行して証明書の更新が不要だった場合でもサービスをreloadしてしまうので、あまり効率的なやり方ではない。前述のcertbot公式ドキュメントにも、更新が必要なかった場合でもcertbot renewの終了ステータスは0(正常)で返ると書かれている。

certbot renew exit status will only be 1 if a renewal attempt failed. This means certbot renew exit status will be 0 if no certificate needs to be updated. If you write a custom script and expect to run a command only after a certificate was actually renewed you will need to use the --deploy-hook since the exit status will be 0 both on successful renewal and when renewal is not necessary.

これで本当に完了だよ。お疲れ様でした!

補足:他にいろいろ調べたことを自分用にメモ

もう「Let's EncryptでPostfix+Dovecotにちゃんとした証明書を設定する」ことは終わったので、これ以下は特に読む必要はありません。

いろいろ試行錯誤している時に調べた内容を残しておきます。

certbot renewで証明書の更新に成功したあと、サービスをreloadするのは本当に必要か

certbot renewによって証明書の更新が成功した場合、証明書ファイルは新しくなるが、postfixdovecotの設定ファイルから見たパスは変わらない。設定ファイルとして変更がない場合、reloadが必要なのかどうかはpostfixdovecotの実装次第な気がする。

certbotの公式サイトは本当に丁寧に書いてあり(探しづらかったが)、ここまでcertbotの説明や機能は本当に至れり尽くせりだった。であれば、証明書更新したあとにサービスのreload的なものが必要ならばそうやって説明してくれていてもよさそうだ。それが書いてないってことは、必ずしも必要とは限らないのでは?

postfixdovecotのマニュアルにも目を通してみたがはっきりとした答えは見つけられなかった。ならば試してみるしかない。

実際に、オレオレ証明書を用意して確認してみた。いったんオレオレ証明書の状態で465番と993番ポート経由で接続してその証明書が使われいることを確認したあと、サーバの鍵は変えずにサーバ証明書だけを新しく作り(これもオレオレ)、差し替えた。サービスをreloadしないまま、また465番と993番ポートでアクセス。

すると、465番(つまりpostfixのほう)では証明書は新しいものに変わっており、993番(つまりdovecotのほう)では証明書は古いままだった。

推測だが、これはおそらく両者のサービスとしての状態の違いによるものだと思う。systemctl で status を見ると、以下のように postfixは active (exited) で、dovecotは active (running) になっているので、この差によるものではないか。

$ sudo systemctl status postfix.service dovecot.service 
● postfix.service - Postfix Mail Transport Agent
   Loaded: loaded (/lib/systemd/system/postfix.service; enabled; vendor preset: enabled)
   Active: active (exited) since Mon 2020-05-04 19:17:27 JST; 1 day 11h ago
 (中略)
● dovecot.service - Dovecot IMAP/POP3 email server
   Loaded: loaded (/lib/systemd/system/dovecot.service; enabled; vendor preset: enabled)
   Active: active (running) since Mon 2020-05-04 19:17:55 JST; 1 day 11h ago
 (略)

まあ理屈はどうあれ、少なくともdovecotのほうはreloadしないと新しくなった証明書を認識してくれないことがわかったので、おとなしくcertbot renewのあとにサービスをreloadすることにした。

cronを使った証明書更新のやり方はどうか

certbotの公式を確認してたとき、こういう記述があった。

https://certbot.eff.org/lets-encrypt/ubuntubionic-other

The command to renew certbot is installed in one of the following locations:

/etc/crontab/
/etc/cron.*/*
systemctl list-timers

つまりすでに説明したsystemdのタイマー方式だけでなく、cronを使ったやり方もcerbotインストール時に実は入っている。 /etc/cron.d/ に、certbot というファイルができているのだ。

/etc/cro.d/certbotの中身。

# /etc/cron.d/certbot: crontab entries for the certbot package
# 
# Upstream recommends attempting renewal twice a day
#
# Eventually, this will be an opportunity to validate certificates
# haven't been revoked, etc.  Renewal will only occur if expiration
# is within 30 days.
#
# Important Note!  This cronjob will NOT be executed if you are
# running systemd as your init system.  If you are running systemd,
# the cronjob.timer function takes precedence over this cronjob.  For
# more details, see the systemd.timer manpage, or use systemctl show
# certbot.timer.
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

0 */12 * * * root test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot -q renew

certbotインストール時に証明書更新の仕組みが勝手に入ってるなんてまだ思ってもいなかったときにたまたまこのファイルが置いてあるのを見つけたので、最初なんでこれがここにあるのか、これはなんなのが全くかわからなかった。ここからさんざんcronについて調べることになる。

すでにできていた /etc/cron.d/certbot の中身を少しずつ読み解いてみた

まず内容としてはcertbot renewを1日2回、0時と12時に行うやつだということはわかるが、コメントに気になる内容が書かれている。「あなたのシステムがsystemdを使っているなら、このcronjobは実行されません」とある。ん、これ実行されないの?

この時点では自分のシステムがsystemdベースになっていることに自信が持ててなかったので、まずこれが実行されるものなのかから確認し始めた。

cron実行のログは /var/log/syslogにも出力されていたけど、「systemdで動かされるプロセスのログは、ファイルではなくjournaldが保持している」という情報を見かけたので、journalctlコマンドを使って確認してみる。

cronの実行ログを見るにはjournalctlコマンドを使う - モヒカンメモ

$ sudo journalctl -u cron
 (中略)
May 05 12:00:01 hostname CRON[13184]: (root) CMD (test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot -q renew)

12時に実行されている。が、cronとしては実行されているという意味で、certbot renew自体は実行されていないかもしれない。

やはり test -x /usr/bin/certbot -a \! -d /run/systemd/system && perl -e 'sleep int(rand(43200))' && certbot -q renew を解読する必要がある。特に前半の部分がまったくわからなかったので、シェルスクリプトに関してまた調べ始める。

このサイトがめちゃめちゃ詳しかった。大変助かりました。ありがとうございます。

if 文と test コマンド | UNIX & Linux コマンド・シェルスクリプト リファレンス

testが条件式を評価するコマンド、-x, -a, -dはtestコマンドのオプションで、

  • -x は「file が実行可能ならば真となる」
  • -d は「file がディレクトリならば真となる」
  • -a はAND条件

そして!はNOT条件、&&はAND条件、とのことだ。

!の前の\が何を意味するのかがわからなかったが、多分エスケープか何かなのかな。

これらをもとにtest -x /usr/bin/certbot -a \! -d /run/systemd/system を解釈すると、「certbot実行ファイルがあり、かつ、/run/systemd/systemがディレクトリとして存在しないなら」ということになる。

自分の環境を確認してみると、/run/systemd/systemディレクトリは存在するので(これはつまりシステムがsystemdで動いていることの判断基準なのだろう)この条件を満たさない。つまりこのcronではcertbot renewは実行されないということだ。

ならば実行されるように変えないといけないなーとさらにいろいろやっていた最中に、systemdのcertbotタイマーがすでに設定されていることを偶然見つけ、あとはすでに述べた通り。

それでもやはりcronでやりたいという場合は

私は結局やらなかったが、どうしてもcronでやりたい場合は先ほどの /etc/cron.d/certbotの中身を変えたバージョンを作ればうまくいくと思う。たぶんこんな感じかな(ためしてないけど)。

SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

0 */12 * * * root perl -e 'sleep int(rand(43200))' && certbot -q renew

--deply-hookオプションをつけて certbot -q renew --deploy-hook "systemctl reload postfix.service dovecot.service" とするもよし、Hookクスリプトを置く方法でもよし。

なお、/etc/cron.d/の下に置くcronのファイルは拡張子をつけてはいけない(拡張子付いているとcron対象外と判断される)らしいです。

また、/etc/cron.d/下にcronのファイルを置いた後、 sudo service cron restart のような感じでcronサービスをリスタートするといった記事もググった感じだと見受けられましたが、少なくとも私の環境ではそれは不要で、ファイルを置いたり変更したりしただけで、勝手に認識されて実行されるようになりました。ログを見てみると、変更を検知したログが出力されていました。

$ sudo journalctl -u cron
 (中略)
May 05 21:34:01 hostname cron[518]: (*system*certbot) RELOAD (/etc/cron.d/certbot)

まあもしかしたらsystemdベースでcronが動いているかどうかでまた違うのかもしれない。