mastodonをGoogle Container Engine(GKE)、Google Cloud SQL、Google Cloud Storage、Redis Labsを使って構築し、pawoo.netの素晴らしいイラストを収集する方法
Google Compute Engineで構築する方法はこちら
公式のProduction-guide、 Docker-Guide を参考に構築します。
Dockerイメージは、単体で全て入りですが、ここでは メディアファイルを溜め込むため、データストアにGoogle Cloud Storageを利用し、 DBにGoogle Cloud SQLでPostgreSQLを利用します。
コンテナの再起動の際の永続化が難しいので、RedisサーバにはRedis Labs のマネージドサーバを利用することとします。
ここでは、メール送信にmailgunを利用します。他のメールサービスや自前のサーバでもかまいません。
$100/mo弱となる。クラスタのnode数を下げるともう少し料金が下がる。
mailgunにアクセスし、アカウントを取得します。 画面に従いドメインの登録を行います。指示に従い、お使いのDNSサーバにエントリを追加しておきます。 クレジットカードの登録をしてConcept Planに昇格させておかないと、メールが送れず困るので登録を忘れずに。 10,000emails/monthまで無料です。
Domainsページから、登録したドメインを選択し、設定をメモします。
Default SMTP LoginとDefault Passwordが後で必要になります。
Google Cloud Storageにアクセスし、バケットを作っておきます。
ここでは、名前を"mastodon"、Regionalを指定し、ロケーションは後でサーバーを立てるリージョンを指定します。
設定の相互運用性に行き、"新しいキーを作成"からアクセスキーと非公開を作成し、メモしておきます。
Google Cloud SQLにアクセスし、インスタンスを作成しておきます。
インスタンスの作成から、データベースエンジンの選択に進み、PostgreSQLを選択します。
次にインスタンスIDを適当に決め、リージョンをサーバーを立てるところに指定しておきます。 マシンタイプは最小の1vCPU, 3.75GBかその下のdb-pg-f1-micro(共有CPU, RAM:0.6GB)で十分です。その他はデフォルトのままでよいです。 デフォルトのユーザーpostgresのパスワードを生成して、記録しておきます。
Redis Labsからアカウントを作成します。
有料のサブスクリプションもありますが、無料版で大丈夫です。無停止で有料サブスクリプションに移行することができるので 必要になったときに購入すると良いでしょう。
有料のサブスクリプションを利用すると、In Memory ReplicationとData Persistenceを利用することができます。 100MB $6/moから
Container Engineのコンテナは、Compute Engine上に構成されたクラスタで実行されます。
コンテナを操作するための、コンソール環境が必要となります。 ローカルの環境にSDKを入れて操作することもできますが、ここではブラウザ上から操作できる Google Cloud Shellを利用して構築することとします。 このシェルは既にSDKがインストールされた状態ですので、立ち上げるとすぐに使用できます。
$ gcloud container clusters create mastodon --zone us-central1-c
任意のリージョンに作成します。ここではus-central-cとします。 クラスタ名はmastodonとしておきます。
$ gcloud container clusters get-credentials mastodon --zone us-central1-c
Google Cloud SQLへのアクセスの際に、IPアドレスで制限することが不可能なので、 サービスアカウントキーでアクセスできるようにします。
https://console.cloud.google.com/apis/credentials にアクセスし、認証情報を作成します。
認証情報を作成からサービスアカウントキーを選択します。
新しいサービスアカウントを選択し、アカウント名に適当な名前を入れます。 役割に、Cloud SQL クライアントを選択します。 作成を押すと、jsonファイルで秘密鍵がダウンロードされます。
ダウンロードされたjsonファイルの内容を、シェルの場所にcredentials.jsonとして保存します。
credentials.jsonをクラスタに保存します。
$ kubectl create secret generic service-account-token --from-file=credentials.json=$HOME/credentials.json
プリコンパイルした内容を保存する永続ディスクを3つ作成します。
以上の3つのディレクトリにマウントされるディスクを作成します。
$ gcloud compute disks create --size 1GB mastodon-disk1 --zone us-central1-c
$ gcloud compute disks create --size 1GB mastodon-disk2 --zone us-central1-c
$ gcloud compute disks create --size 1GB mastodon-disk3 --zone us-central1-c
大きさは1GBも要りませんが、1GBからしか作成できません。名前は適宜決めてください。
mastodonのimageをローカルに取得します。
$ docker pull gargron/mastodon:v2.4.0
https://github.com/tootsuite/mastodon/releases を参照して最新のタグを指定します。masterは時折壊れているようなので、タグを指定した方が無難です。
クラスターで参照できるようにContainer Registryに保存します。
まずタグをつけます。
$ docker tag gargron/mastodon:v2.4.0 gcr.io/project-id/mastodon:v2.4.0
タグの確認
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
gargron/mastodon v2.3.3 956d59c7c572 7 weeks ago 1.26GB
gcr.io/project-id/mastodon v2.3.3 956d59c7c572 7 weeks ago 1.26GB
pushする
gcloud docker -- push gcr.io/project-id/mastodon
secretを作成するために、以下のコマンドを2回実行して結果を記録しておきます。
$ docker run --rm -it gargron/mastodon:v2.4.0 rake secret
mastodon/.env.production.sampleを参考に、yamlファイルを作成します。
apiVersion: v1
kind: ConfigMap
metadata:
name: env-production
data:
# Service dependencies
#Redis Labsで指定されたホスト、ポート、設定したパスワードを入れる
REDIS_HOST: redis-xxxxx.c1.us-central1-2.gce.cloud.redislabs.com
REDIS_PORT: '12549'
REDIS_PASSWORD: password
#Google Cloud SQLで設定したパスワードを入れる
DB_HOST: db-host
DB_USER: postgres
DB_NAME: postgres
DB_PASS: password
DB_PORT: '5432'
# Federation
LOCAL_DOMAIN: mastodon.example.com
# Application secrets
SECRET_KEY_BASE: (docker run --rm -it gargron/mastodon:v2.4.0 rake secret)の結果1
OTP_SECRET: (docker run --rm -it gargron/mastodon:v2.4.0 rake secret)の結果2
# E-mail configuration
SMTP_SERVER: smtp.mailgun.org
SMTP_PORT: '2525'
SMTP_LOGIN: 設定画面で確認できるDefault SMTP Login
SMTP_PASSWORD: 設定画面で確認できるDefault Password
SMTP_FROM_ADDRESS: mastodon@example.com
# S3 (Minio Config (optional) Please check Minio instance for details)
#Google Cloud Storageの設定
S3_ENABLED: 'true'
S3_BUCKET: mastodon
AWS_ACCESS_KEY_ID: アクセスキー
AWS_SECRET_ACCESS_KEY: シークレット
S3_REGION: us-central1
S3_PROTOCOLi: https
S3_HOSTNAME: storage.googleapis.com
S3_ENDPOINT: https://storage.googleapis.com
S3_SIGNATURE_VERSION: s3
# Cluster number setting for streaming API server.
STREAMING_CLUSTER_NUM: '1'
.env.production.sampleを参考に作成したenv.production.yamlを登録します。
$ kubectl apply -f env.production.yaml
GKEのIPアドレスの範囲を特定することが難しいので、sql-proxyを介してGoogle Cloud SQLにアクセスするように設定します。
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: db-host
spec:
replicas: 1
template:
metadata:
labels:
app: db-host
spec:
containers:
- image: b.gcr.io/cloudsql-docker/gce-proxy
name: db-host
command:
- /cloud_sql_proxy
- -dir=/cloudsql
- -instances=project-id:us-central1:mastodon-db=tcp:0.0.0.0:5432
- -credential_file=/credentials/credentials.json
ports:
- name: mastodon-sql
containerPort: 5432
volumeMounts:
- mountPath: /cloudsql
name: cloudsql
- mountPath: /etc/ssl/certs
name: ssl-certs
readOnly: true
- mountPath: /credentials
name: service-account-token
volumes:
- name: cloudsql
emptyDir:
- name: ssl-certs
hostPath:
path: "/etc/ssl/certs"
- name: service-account-token
secret:
secretName: service-account-token
---
apiVersion: v1
kind: Service
metadata:
name: db-host
spec:
type: ClusterIP
ports:
- port: 5432
targetPort: mastodon-sql
selector:
app: db-host
次のコマンドで設定し、起動します。
$ kubectl apply -f backend.yaml
mastodonのDBを初期化します。 vapid_keyを生成します。
apiVersion: batch/v1
kind: Job
metadata:
name: web-init1
spec:
template:
metadata:
name: web-init1
spec:
containers:
- name: web-init1
image: gcr.io/project-id/mastodon:v2.4.0
envFrom:
- configMapRef:
name: env-production
command: ["bash","-c","rake db:migrate && rake mastodon:webpush:generate_vapid_key"]
restartPolicy: Never
assetのプリコンパイルを行います。
apiVersion: batch/v1
kind: Job
metadata:
name: web-init2
spec:
template:
metadata:
name: web-init2
spec:
securityContext:
fsGroup: 991 # container user gid
containers:
- name: web-init2
image: gcr.io/project-id/mastodon:v2.4.0
envFrom:
- configMapRef:
name: env-production
command: ["rake", "assets:precompile"]
volumeMounts:
- name: mastodon-disk1
mountPath: /mastodon/public/system
- name: mastodon-disk2
mountPath: /mastodon/public/assets
- name: mastodon-disk3
mountPath: /mastodon/public/packs
volumes:
- name: mastodon-disk1
gcePersistentDisk:
pdName: mastodon-disk1
fsType: ext4
readOnly: false
- name: mastodon-disk2
gcePersistentDisk:
pdName: mastodon-disk2
fsType: ext4
readOnly: false
- name: mastodon-disk2
gcePersistentDisk:
pdName: mastodon-disk2
fsType: ext4
readOnly: false
restartPolicy: Never
assetのプリコンパイルは、バージョンアップの際には別のディスクを作成する必要があります。 GKEでは複数のポッドからディスクをマウントするにはリードオンリーでなければなりませんが、 起動中のポッドがロックしているので、書き込み可能で再マウントすることができません。
次にコマンドで実行します。
$ kubectl apply -f web-init1.yaml
$ kubectl apply -f web-init2.yaml
実行しているpodの名前を確認します。
$ kubectl get pod -a
NAME READY STATUS RESTARTS AGE
db-host-5c7c457d88-2mxxh 1/1 Running 0 6h
web-init1-87wgs 1/1 Running 0 4s
web-init1の方のpodのログを確認し、生成されたvapid_keyを確認します。
$ kubectl logs -f web-init1-87wgs
(中略)
VAPID_PRIVATE_KEY=us1Hxx4nXwy4HCXsb50_NJaUKpQqRzFSTS1FgUyG9RI=
VAPID_PUBLIC_KEY=BBYlb7l6lKulKPwbkrXK3SdF7LJ94jWKpLqxf2qJjQsSmELeP1791vP41aPiAEjx96bxngMD50SNEgbwWkLMvoY=
vapid_keyをenv.production.yamlに追記します。
VAPID_PRIVATE_KEY: us1Hxx4nXwy4HCXsb50_NJaUKpQqRzFSTS1FgUyG9RI=
VAPID_PUBLIC_KEY: BBYlb7l6lKulKPwbkrXK3SdF7LJ94jWKpLqxf2qJjQsSmELeP1791vP41aPiAEjx96bxngMD50SNEgbwWkLMvoY=
この2行を追記
env.production.yamlを更新します。
$ kubectl apply -f env.production.yaml
完了したら、消しておきます。
$ kubectl delete -f web-init1.yaml
$ kubectl delete -f web-init2.yaml
nginx.confを作成します。
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
listen [::]:80;
server_name mastodon.example.com;
access_log /dev/stdout;
location /healthz {
return 200 "healthy\n";
}
if ( $http_user_agent ~* googlehc ) {
return 200 "healthy\n";
}
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
keepalive_timeout 70;
sendfile on;
client_max_body_size 0;
root /mastodon/public;
gzip on;
gzip_disable "msie6";
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
add_header Strict-Transport-Security "max-age=31536000";
location / {
try_files $uri @proxy;
}
location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri @proxy;
}
location /sw.js {
add_header Cache-Control "public, max-age=0";
try_files $uri @proxy;
}
location @proxy {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Proxy "";
proxy_pass_header Server;
proxy_pass http://127.0.0.1:3000;
proxy_buffering off;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
tcp_nodelay on;
}
location /api/v1/streaming {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Proxy "";
proxy_pass http://127.0.0.1:4000;
proxy_buffering off;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
tcp_nodelay on;
}
error_page 500 501 502 503 504 /500.html;
}
設定をロードします。
$ kubectl create configmap nginx-config --from-file=nginx.conf
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 2
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: nginx
image: nginx
ports:
- containerPort: 80
livenessProbe:
httpGet:
path: /healthz
port: 80
initialDelaySeconds: 5
timeoutSeconds: 1
volumeMounts:
- mountPath: /etc/nginx/conf.d
name: nginx-config
readOnly: true
- name: web
image: gcr.io/project-id/mastodon:v2.4.0
envFrom:
- configMapRef:
name: env-production
command:
- bundle
- exec
- rails
- s
- -p
- '3000'
- -b
- '0.0.0.0'
ports:
- containerPort: 3000
volumeMounts:
- name: mastodon-disk1
mountPath: /mastodon/public/system
readOnly: true
- name: mastodon-disk2
mountPath: /mastodon/public/assets
readOnly: true
- name: mastodon-disk3
mountPath: /mastodon/public/packs
readOnly: true
- name: streeming
image: gcr.io/project-id/mastodon:v2.4.0
envFrom:
- configMapRef:
name: env-production
command:
- yarn
- start
ports:
- containerPort: 4000
volumes:
- name: mastodon-disk1
gcePersistentDisk:
pdName: mastodon-disk1
fsType: ext4
readOnly: true
- name: mastodon-disk2
gcePersistentDisk:
pdName: mastodon-disk2
fsType: ext4
readOnly: true
- name: mastodon-disk3
gcePersistentDisk:
pdName: mastodon-disk3
fsType: ext4
readOnly: true
- name: nginx-config
configMap:
name: nginx-config
---
apiVersion: v1
kind: Service
metadata:
name: frontend
labels:
app: frontend
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
selector:
app: frontend
次のコマンドで起動します。
$ kubectl apply -f frontend.yaml
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: sidekiq
spec:
replicas: 1
template:
metadata:
labels:
app: sidekiq
spec:
containers:
- name: sidekiq
image: gcr.io/project-id/mastodon:v2.4.0
envFrom:
- configMapRef:
name: env-production
command:
- bundle
- exec
- sidekiq
- -q
- default
- -q
- mailers
- -q
- pull
- -q
- push
次のコマンドで起動します。
$ kubectl apply -f sidekiq.yaml
$ gcloud compute addresses create kubernetes-ingress --global
$ gcloud compute addresses list
DNSにセットしておきます。
まずhttpでアクセスできるように設定します。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mastodon
annotations:
kubernetes.io/ingress.global-static-ip-name: kubernetes-ingress
labels:
app: mastodon
spec:
backend:
serviceName: frontend
servicePort: 80
次のコマンドで適用します。
$ kubectl apply -f ingress.yaml
しばらくすると(数分)ロードバランサにIPアドレスがセットされます。 次のコマンドで確認することができます。 この状態でhttpでアクセスすると、httpsにリダイレクトされエラーで止まります。
$ kubectl get ingress
次にhttpでアクセスできるように設定します。
コンソールにhelmをインストールします。 let's encryptの証明書を適切に処理してくれるcert-manager をインストールするのに必要です。
$ wget https://storage.googleapis.com/kubernetes-helm/helm-v2.9.1-linux-amd64.tar.gz
$ tar xvf helm-v2.9.1-linux-amd64.tar.gz
$ cp linux-amd64/helm .
クラスタにhelmをインストールします。
$ kubectl create serviceaccount tiller --namespace kube-system
$ kubectl create clusterrolebinding tiller --clusterrole=cluster-admin --serviceaccount=kube-system:tiller
$ ./helm init --upgrade --service-account tiller
クラスタにcert-managerをインストールします。
$ ./helm install --name cert-manager --namespace kube-system stable/cert-manager
証明書の更新の連絡を受けるメールアドレスを設定します。
$ export EMAIL=ahmet@example.com
Issuer manifestsの登録をします。
$ curl -sSL https://rawgit.com/ahmetb/gke-letsencrypt/master/yaml/letsencrypt-issuer.yaml | \
sed -e "s/email: ''/email: $EMAIL/g" | \
kubectl apply -f-
ロードバランサにIPアドレスが振られて、httpでアクセスできるようになったら、 証明書の取得を行います。
apiVersion: certmanager.k8s.io/v1alpha1
kind: Certificate
metadata:
name: mastodon-tls
namespace: default
spec:
secretName: mastodon-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
commonName: mastodon.example.com
dnsNames:
- mastodon.example.com
acme:
config:
- http01:
ingress: mastodon
domains:
- mastodon.example.com
次のコマンドで適用します。
$ kubectl apply -f certificate.yaml
証明書の取得には10−20分程度の時間がかかります。コーヒーの時間です。 次のコマンドで/.well-known/acme-challenge/*が自動生成されていることが 確認できます。
$ kubectl get ingress -o=yaml mastodon
証明書の取得が完了すると、次のコマンドのように秘密鍵がセットされています。
$ kubectl get secrets
NAME TYPE DATA AGE
default-token-spvst kubernetes.io/service-account-token 3 15h
mastodon-tls kubernetes.io/tls 2 10h
service-account-token Opaque 1 9h
証明書がセットされたのでhttpsでアクセスできるように修正します。
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: mastodon
annotations:
kubernetes.io/ingress.global-static-ip-name: kubernetes-ingress
labels:
app: mastodon
spec:
backend:
serviceName: frontend
servicePort: 80
tls:
- secretName: mastodon-tls
hosts:
- mastodon.example.com
次のコマンドで適用します。
$ kubectl apply -f ingress.yaml
ロードバランサに適用されるまで数分かかります。 完了すればドメインにアクセスするとMastodonのページが見えます。お疲れ様でした。
管理者のアカウントを作成します。 まず、frontendのpodを確認します。
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
db-host-5c7c457d88-2mxxh 1/1 Running 0 10h
frontend-56f7dbfbd8-58rmz 3/3 Running 0 2h
frontend-56f7dbfbd8-pzbvs 3/3 Running 0 2h
sidekiq-d94c6b9cd-pvfj9 1/1 Running 0 3h
frontendのpodのIDを探して次のコマンドを実行します。
$ kubectl exec -it frontend-56f7dbfbd8-58rmz -c web -- rails c
railsのコンソールが起動するので、以下のように管理者アカウントを設定します。
irb(main):001:0> password = SecureRandom.hex(16)
=> "63f7b0523db470effb7a560050cddac4" (自動生成のパスワードを記録しておきます。もしくは任意のパスワードを設定します)
irb(main):002:0> username = 'admin'
=> "admin"
irb(main):003:0> email = '有効なメールアドレス’
=> "admin@example.com”
irb(main):004:0> user = User.new(admin: true, email: email, password: password, confirmed_at: Time.now.utc, account_attributes: { username: username })
=> #<User email: "admin@example.com", created_at: nil, updated_at: nil, admin: true, locale: nil, encrypted_otp_secret: nil, encrypted_otp_secret_iv: nil, encrypted_otp_secret_salt: "_aRlexnt3IPpJKyWd5emjQg==\n", consumed_timestep: nil, otp_required_for_login: false, last_emailed_at: nil, otp_backup_codes: nil, filtered_languages: [], account_id: nil, id: nil, disabled: false, moderator: false, invite_id: nil, otp_secret: nil>
irb(main):005:0> user.save(validate: false)
=> true
irb(main):006:0> exit