Notionの無料利用枠を使い切ってしまったのでAppflowyに移行したかった。けど結局Google ドキュメントに移行した。
移行を検討した背景
Notionをプライベートで結構便利に利用していました。
特に家庭内Wikiみたいな使い方をするにあたり、カレンダー等含め情報が集約されること、UIが使いやすいことなどが相まって非常に便利だったのですが、NotionのFree版では2人以上の共有ワークスペースでは1000ブロックまでという制限が存在します。
この制限は承知していて、以前はワークスペースを共有しているアカウントでもPrivateに配置しているものはカウント対象外だったり、ブロックを削除した場合は制限がまた戻るというのでやりくりしていたのですが、いつからか(もしかすると上のやりくり自体実は最初からできず、私が勘違いしていただけという可能性もありますが)、
共有ワークスペースで制限に達すると自分のPrivateも使えなくなったり、何かを削除しても制限が解除されなくなってしまいました。
こうなると何も書き込めず文鎮と化してしまうので、移行先を探し始めたという流れです。
尚、Notion自体は非常に便利で、もしこれをお仕事に使ったりする場合であれば、2000円/人・月の費用を払うのもやぶさかでないのですが、
さすがにPrivate用途でこのお値段はつらい、、、ということで移行したいと考えてこの検討を始めました。
(画像とか月あたりのブロック数厳しめで、3$/人とかそのぐらいのプランがあれば、、、)
なお、ちゃんと動かすところまでやり切ったのですが、オチとしてはGoogleドキュメントに移行したというオチです。
移行先の選定
Notion的なOpenSourceには、有名どころとしてOutline, AppFlowy, Affineが存在します。
この中ではOutlineが最も古参であり、発足したタイミングを見る限りではNotionよりはConfluenceを意識して作られたようなものに見ます。
Appflowy, Affineについては明確にNotionを意識して始まったプロジェクトであり、Affineが最も新しいです。
とはいえAppflowyもまだまだ若いプロダクトであり、安定したとは言い切りがたい状況ではあります。
今回はNotionからの移行ということで、AppFlowy, Affineのどちらかに焦点を絞って比較し、
* Affineよりは1年程度古く、少しは枯れていそうなところ。
* AffineはMiro的な機能も取り込もうとしており、多機能思想のように見え、少し安定感が気になった事。
* ドキュメントがAppFlowyの方が整備されていること
から、Appflowyを選定しました。
やること
ということで、実構築に入っていくのですが、まずはやることをまとめておきます。
結構多いですが、まぁ仕方ない。
* ドメインの取得
* AWS基盤の準備
* 証明書の取得
* AppFlowy Cloudのインストールと起動
* 証明書の取得
* Appflowy Nginx設定の変更
* 動作確認
* セキュリティ系の設定変更
* Dockerのネットワーク設定をipv6に変更
* GCP(認証用)の準備
* AppFlowy起動と動作確認
ドメインの取得
Appflowyのホスティングにあたり、HTTPS化したいので、ドメインをとっておきます。(オレオレ証明書でもよいのですが、Appflowyの場合認証失敗したときどうなるかがよくわからないので)
お名前ドットコムとかで取得するのも管理するサイトが増えて面倒なので、AWSでそのままドメインを購入します。
コストは最新のものを都度見る必要がありますが、最安は(多分).clickの$3、.linkの$5あたり。ただしここら辺の超新興ドメインが少し心配という場合は、.comの$14 .netの$15あたりでしょうか。
ぶっちゃけた話今時怪しいサイトでもいっちょ前に.comあたりをとることが殆どだと思うので(じゃないと引っかかってくれない)特に気にせずに.clickをとります。
更新費用の値上がりなんかを気にすることを考えると、.clickを雑に10年とか買うのもいいんじゃないでしょうか。(30$ = 4500円くらいで10年持つので)
AWS基盤の準備
ということで、Appflowy稼働用の基盤を用意します。
商用じゃないので、めっちゃ簡単な基盤にします。
なお、最近Ipv4のPublic IPも有料化され、これが意外と高いので、Ipv6で構成します。
あとこれ地味に知らなかったんですが、Ipv6だとインスタンス停止しても特にリリースされないみたいです。固定どうしようかなと思っていたので結構便利な仕様。
IPv6 アドレスは、インスタンスの停止して起動、または休止して起動する際には保持され、インスタンスの終了時にリリースされます。
・VPC・サブネット(Ipv6対応)
・EC2インスタンス(スポットインスタンス・Instance Connectで接続。AppFlowyの推奨に合わせてt4g.mediumにしています。)
・AWS Backup(EC2インスタンスバックアップ用)
・EventBridge Scheduler(深夜とかの明らかに使わない時間の自動停止用)
・EC2ステータスチェックのアラーム(ほぼ監視する気ないですが、さすがに最低限として。本当はディスク残見た方がいいのですが、さすがにCloudWatch Agent入れるのは面倒で、、、)
・S3バケット(EC2へのファイル受け渡し用。Ipv6だとGithubとかつながらないという問題が、、、)
尚、CFnではSessionManager使うつもりのRoleがEC2についています。もともとSSMでつなぐつもりだったのですが、まさかのIpv6未対応、、、ただInstance ConnectはIpv6に対応しているので、Instance Connectの東京リージョンPrefixリストを開けるようにしてあります。
ということで、GPTにぶん投げつつ作ったCFnは以下の通りです。
余談ですが、GPT3.5とかに任せると、絶妙にパラメタ名が違ったり、階層が違ったりする事象が発生し、意外とデバッグがつらかったです、、、(基盤程度のサイズならいいのですが、本格的なソフトウェア開発にはまだだいぶ辛そうだなぁ、、と)
AMI IDは好きなのを入れてください。(この後の作業はAL2023-Arm前提で進めますので一応推奨は最新のg系インスタンスに対応してAmazon Linux 2023です。)
CFn中にもコメント残していますが、スポットリクエストがCFnを削除しても残存する場合があるようなので、作成・削除をやる場合は留意してください。
CloudFormation テンプレート
AWSTemplateFormatVersion: '2010-09-09'
Description: Create a InfraStructure for Appflowy
Parameters:
InstanceAMIID:
Description: AMI ID for EC2 Instance
Type: String
ScheduleStartTime:
Type: String
Description: JST
Default: "cron(0 10 * * ? *)"
ScheduleStopTime:
Type: String
Description: JST
Default: "cron(0 1 * * ? *)"
BackupPlanName:
Type: String
Default: "MyBackupPlan"
Description: Name of the backup plan
BackupVaultName:
Type: String
Default: "MyBackupVault"
Description: Name of the backup vault
SnsTopicName:
Type: String
Default: "EC2StatusCheckAlerts"
Description: Name of the SNS topic for alerts
S3BucketName:
Type: String
Resources:
S3Bucket:
Type: 'AWS::S3::Bucket'
Properties:
BucketName: !Ref S3BucketName
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
LifecycleConfiguration:
Rules:
- ExpirationInDays : 3
Prefix: ""
Status: "Enabled"
MyVPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: MyVPC
AddIPv6CIDR:
Type: AWS::EC2::VPCCidrBlock
Properties:
AmazonProvidedIpv6CidrBlock: true
VpcId: !Ref MyVPC
MySubnet:
DependsOn: AddIPv6CIDR
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref MyVPC
AvailabilityZone: "ap-northeast-1a"
AssignIpv6AddressOnCreation: true
Ipv6CidrBlock: !Select [ 0, !Cidr [ !Select [ 0, !GetAtt MyVPC.Ipv6CidrBlocks], 1, 64 ]]
Ipv6Native: true
Tags:
- Key: Name
Value: MyIPv6Subnet
MyInternetGateway:
Type: AWS::EC2::InternetGateway
Properties: {}
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref MyVPC
InternetGatewayId: !Ref MyInternetGateway
MyRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref MyVPC
Tags:
- Key: Name
Value: MyRouteTable
MyRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref MyRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref MyInternetGateway
MyIPv6Route:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref MyRouteTable
DestinationIpv6CidrBlock: ::/0
GatewayId: !Ref MyInternetGateway
SubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref MySubnet
RouteTableId: !Ref MyRouteTable
MySecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Security group allowing inbound TCP traffic on port 443 for IPv6
VpcId: !Ref MyVPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIpv6: ::/0 # IPv6アドレスからのトラフィックを許可
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIpv6: ::/0 # IPv6アドレスからのトラフィックを許可
# インスタンスコネクト用
- IpProtocol: tcp
FromPort: 22
ToPort: 22
SourcePrefixListId: "pl-012493c5f82b88e8e"
SecurityGroupEgress:
- IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIpv6: ::/0 # IPv6アドレスからのトラフィックを許可
Tags:
- Key: Name
Value: MySecurityGroup443
EC2Role:
Type: AWS::IAM::Role
Properties:
RoleName: AppflowyEC2Role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: "allowUseOfTheS3Bucket"
PolicyDocument:
Version: 2012-10-17
Statement:
- Sid: s3Access
Effect: Allow
Action:
- 's3:List*'
- 's3:GetObject*'
Resource: '*'
- Sid: s3Put
Effect: Allow
Action:
- 's3:PutObject*'
Resource: !Sub '${S3Bucket.Arn}/*'
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
# 尚、SpotリクエストはCloudFormation管轄外となり、CloudFormationを消しても消えないことがあるらしいので要注意。
ECSplotLaunchTemplate:
Type: AWS::EC2::LaunchTemplate
Properties:
LaunchTemplateData:
InstanceType: "t4g.medium"
InstanceMarketOptions:
MarketType: spot
SpotOptions:
InstanceInterruptionBehavior: stop
MaxPrice: !Ref 'AWS::NoValue' # OnDemand
SpotInstanceType: persistent
MyEC2Instance:
Type: AWS::EC2::Instance
Properties:
ImageId: !Ref InstanceAMIID
IamInstanceProfile: !Ref EC2InstanceProfile
SecurityGroupIds:
- !Ref MySecurityGroup
SubnetId: !Ref MySubnet
LaunchTemplate:
LaunchTemplateId: !Ref ECSplotLaunchTemplate
Version: !GetAtt ECSplotLaunchTemplate.LatestVersionNumber
# IAM Instance Profile
EC2InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref EC2Role
# https://dev.classmethod.jp/articles/cloudformation-template-eventbridge-scheduler-ec2-start-stop/
ScheduleEC2Sart:
Type: AWS::Scheduler::Schedule
Properties:
Name: !Sub 'EC2-Start-${MyEC2Instance}'
Description: Start EC2 Instance
ScheduleExpression: !Ref ScheduleStartTime
ScheduleExpressionTimezone: "Japan"
FlexibleTimeWindow:
Mode: "OFF"
State: ENABLED
Target:
Arn: arn:aws:scheduler:::aws-sdk:ec2:startInstances
Input: !Sub |-
{
"InstanceIds": ["${MyEC2Instance}"]
}
RoleArn:
Fn::GetAtt:
- SchedulerEC2StopStartRole
- Arn
ScheduleEC2Stop:
Type: AWS::Scheduler::Schedule
Properties:
Name: !Sub 'EC2-Stop-${MyEC2Instance}'
Description: Stop EC2 Instance
ScheduleExpression: !Ref ScheduleStopTime
ScheduleExpressionTimezone: "Japan"
FlexibleTimeWindow:
Mode: "OFF"
State: ENABLED
Target:
Arn: arn:aws:scheduler:::aws-sdk:ec2:stopInstances
Input: !Sub |-
{
"InstanceIds": ["${MyEC2Instance}"]
}
RoleArn:
Fn::GetAtt:
- SchedulerEC2StopStartRole
- Arn
SchedulerEC2StopStartRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- scheduler.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
Policies:
- PolicyName: EC2StopStart
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- ec2:StartInstances
- ec2:StopInstances
Resource:
- "*"
BackupVault:
Type: AWS::Backup::BackupVault
Properties:
BackupVaultName: !Ref BackupVaultName
# Backup Plan
BackupPlan:
Type: AWS::Backup::BackupPlan
Properties:
BackupPlan:
BackupPlanName: !Sub '${BackupPlanName}-Daily'
BackupPlanRule:
- RuleName: "DailyBackup"
TargetBackupVault: !Ref BackupVault
ScheduleExpression: "cron(0 0 * * ? *)" # 毎日00:00 UTC
Lifecycle:
DeleteAfterDays: 2 # 2日後に削除
BackupPlanWeekly:
Type: AWS::Backup::BackupPlan
Properties:
BackupPlan:
BackupPlanName: !Sub '${BackupPlanName}-Weekly'
BackupPlanRule:
- RuleName: "WeeklyBackup"
TargetBackupVault: !Ref BackupVault
ScheduleExpression: "cron(0 0 ? * 1 *)" # 毎週日曜日00:00 UTC
Lifecycle:
DeleteAfterDays: 15 # 15日後に削除
# Backup Selection
BackupSelection:
Type: AWS::Backup::BackupSelection
Properties:
BackupPlanId: !Ref BackupPlan
BackupSelection:
SelectionName: "MyBackupSelection"
IamRoleArn: !GetAtt BackupRole.Arn
Resources:
- !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/${MyEC2Instance}"
BackupSelectionWeekly:
Type: AWS::Backup::BackupSelection
Properties:
BackupPlanId: !Ref BackupPlanWeekly
BackupSelection:
SelectionName: "MyBackupSelectionWeekly"
IamRoleArn: !GetAtt BackupRole.Arn
Resources:
- !Sub "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/${MyEC2Instance}"
# IAM Role for AWS Backup
BackupRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: backup.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores
- arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup
Policies:
- PolicyName: BackupPermissions
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- "iam:PassRole"
Resource: !GetAtt EC2Role.Arn
- Effect: Allow
Action:
- "iam:ListRoles"
Resource: "*"
SnsTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: !Ref SnsTopicName
# CloudWatch Alarm for EC2 Status Checks
StatusCheckAlarm:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: !Sub "${MyEC2Instance}-StatusCheckAlarm"
MetricName: StatusCheckFailed
Namespace: AWS/EC2
Statistic: Sum
Period: 300 # 5分間隔
EvaluationPeriods: 1
Threshold: 1
ComparisonOperator: GreaterThanThreshold
Dimensions:
- Name: InstanceId
Value: !Ref MyEC2Instance
AlarmActions:
- !Ref SnsTopic
OKActions:
- !Ref SnsTopic
AlarmDescription: "Alarm when EC2 status check fails"
AppFlowy Cloudのインストールと起動
この後証明書とかを入れるのですが、まずは一度AppFlowy Cloudを立ち上げておきます。
最低限の動作確認です。
基本的には以下のドキュメントに従ってDeployしたいのですが、、、
GithubがIpv6に対応していないという致命的な問題があるので、手で後ほどファイルを持ってくることになります。
さてまずAL2023にはdockerもdocker composeもないので、インストールしておきます。
とりあえずdockerを入れてenableまでしておきます。
sudo dnf install -y docker
sudo systemctl start docker
sudo systemctl enable docker
sudo systemctl status docker
次にDocker Composeを入れるのですが、、、GithubがIpv6に対応していないという致命的な問題があるため、スタンドアロン版を手で取ってきた後、S3バケットへアップロードし、S3バケットからEC2へ渡します。
↓から対応するバイナリをとってきて、S3へ上げ
S3からコピーしたうえで配置して、実行権限を渡す。
また、ec2-userをdocker groupに入れて、dockerの権限を渡しておきます。そのあといったんreboot。
最後にバージョン確認。
docker composeをプラグインとして、ec2-userだけに渡す方がよいとは思うのですが、後述のcertbotとの権限のコントロールがちょっと面倒になるので、standaloneとして全ユーザに利用可能にします。
尚、s3 cpの時にendpointでipv6に対応した方を指定してあげないとつながらないので注意。
sudo aws s3 cp s3://<バケット名>/docker-compose-linux-aarch64 /usr/local/bin/docker-compose --endpoint-url https://s3.dualstack.ap-northeast-1.amazonaws.com
sudo chmod +x /usr/local/bin/docker-compose
sudo usermod -aG docker ec2-user
sudo reboot
docker-compose version
次にappflowyの作業用フォルダを切って、再びappflowyのmainブランチをs3経由で持ってきた後に解凍します。(Githubがipv6に対応してさえいれば、、、)
mkdir ~/appflowy
aws s3 cp s3://<バケット名>/AppFlowy-Cloud-main.zip . --endpoint-url https://s3.dualstack.ap-northeast-1.amazonaws.com
unzip AppFlowy-Cloud-main.zip
cd AppFlowy-Cloud-main/
ここまでくればおおよそドキュメントに従って進めることができます。
以下の通りでdocker起動
cp deploy.env .env
docker compose up -d
さてここで残ディスクを確認しておきます。
尚、私の環境ではここまででざっくり50%くらいの消費となったので、すぐすぐ拡張の必要性はなさそうでしたが、様子を見てディスク拡張してください。
df
確認が終わったら、docker落としておきます。
docker compose down
証明書の取得
証明書は前半で取得したドメインを元に、Let's Encryptからとります。
そのため、AL2023にCertbot入れます。(snapdが対応していないので、pip経由)
以下リンクをベースとしつつ、Appflowyのコンテナで稼働しているNginxに対してHTTP-01 Challengeでの更新を構成するのはいろいろ面倒(Ipv6周りでまた出来なくなりそうだし)というのがあるため、standaloneでの作成・更新とします。(なので証明書更新のタイミングで一瞬Appflowyを止める必要が出てきます。が、仕方なし。なおDNS-01 Challengeができればよかったのですが、Route53のPublicエンドポイントがまだIpv6に対応していなかったため諦めました、、、)
それに伴い、python3を、python3.11と読み替えて進めます。
手順6のシンボリックリンクを張ってパスを通すところまでをおおよそベースとして進めます。
sudo dnf install -y python3.11 augeas-libs
sudo python3.11 -m venv /opt/certbot/
sudo /opt/certbot/bin/pip install --upgrade pip
sudo /opt/certbot/bin/pip install certbot
sudo ln -s /opt/certbot/bin/certbot /usr/bin/certbot
ここまでで、certbotコマンドが使用できるようになっているはずなので、AppflowyのNginx証明書パスに証明書を作成します。
まずは自サーバのipv6をPublicレコードとして、Route53に登録しておきます。(使用したいドメイン->Appflowyを起動するEC2インスタンスのAAAAレコードを作成しておきます。)
そうしたら、証明書発行の前に、Appflowyのリポジトリにあらかじめある証明書を削除しておきます。(削除しておかないと、証明書の上書きができず、不正な証明書になってしまう)
rm /home/ec2-user/appflowy/AppFlowy-Cloud-main/nginx/ssl/private_key.key
rm /home/ec2-user/appflowy/AppFlowy-Cloud-main/nginx/ssl/certificate.crt
そのうえで以下の通り実施し、証明書を発行します。
To use your own SSL certificates for https, replace certificate.crt and private_key.key with your own in nginx/ssl/ directory.
https://github.com/AppFlowy-IO/AppFlowy-Cloud/blob/main/doc/DEPLOYMENT.md
sudo certbot certonly --standalone -d <証明書を発行するFQDN>
発行した証明書について、certbotでは最初から別の場所に発行するのが難しいので、発行完了ログの証明書と秘密鍵にシンボリックリンクを貼っておきます。
ln -s /etc/letsencrypt/live/<ドメイン名>/fullchain.pem /home/ec2-user/appflowy/AppFlowy-Cloud-main/nginx/ssl/certificate.crt
ln -s /etc/letsencrypt/live/<ドメイン名>/privkey.pem /home/ec2-user/appflowy/AppFlowy-Cloud-main/nginx/ssl/private_key.key
さて、更新用の設定も修正しておきます。certbotの証明書更新設定は、/etc/letsencrypt/renewal/<ドメイン名>.confにあります。
Appflowyを落としてから更新、その後立ち上げるのでpre_hookとpost_hookを不格好ですが書いておきます。ぶっちゃけこれはうまくいくかわかりません。(本チャンの更新走るまで動作確認が取れないので、、、)
[renewalparams]
<もともとあった設定に追記>
pre_hook = docker-compose down
post_hook = (cd /home/ec2-user/appflowy/AppFlowy-Cloud-main/ && docker-compose up -d)
そしたら、certbot renewをカレンダーsystemd.timerでカレンダー設定しておきます。
※動確まで見れてないので、もしかしたら動かないかもです。すみません。
[Unit]
Description = CertBot Renew Certification
After=network-online.target
[Service]
Type=oneshot
ExecStart=certbot renew
[Install]
WantedBy=multi-user.target
[Unit]
Description=Timer for renew_cert.service
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
そしたら、timerを有効&開始しておきます。
sudo systemctl start renew_cert.timer
sudo systemctl enable renew_cert.timer
sudo systemctl status renew_cert.timer
いったんstatusがActiveになっていればOK。
Appflowy Nginx設定の変更
AppflowyのNginxの設定を変更して、Ipv6を受け入れられるようにしておきます。
また、証明書に合わせてserver_nameも設定しておきます。
追加部分だけ抜粋
ssl_certificate /etc/nginx/ssl/certificate.crt;
ssl_certificate_key /etc/nginx/ssl/private_key.key;
server_name <ドメイン名>; //追加
listen 80;
listen 443 ssl;
listen [::]:443 ssl; //追加
動作確認
ここまででいったんWeb版が使えるようになっているはずなので、
https://<ドメイン>
にアクセスして、動作確認します。
以下のような画面が表示されていれば大丈夫です。
セキュリティ系の設定変更
Appflowyデフォルトの設定ファイルは明らかに弱い部分があるので、補強します。
最低でも以下を適切に変更します。
# PostgreSQL Settings
POSTGRES_HOST=postgres
POSTGRES_USER=postgres
POSTGRES_PASSWORD=changepassword
# Postgresのパスワード。変えておきましょう。
# Supabase user settings
SUPABASE_PASSWORD=root
# 同様。
# authentication key, change this and keep the key safe and secret
# self defined key, you can use any string
GOTRUE_JWT_SECRET=hello456
#ここを変えます。結構長くて複雑なものにした方がよいです。
# You would then need to set GOTRUE_MAILER_AUTOCONFIRM=false
# Check for logs in gotrue service if there are any issues with email confirmation
# Note that smtps will be used for port 465, otherwise plain smtp with optional STARTTLS
GOTRUE_SMTP_HOST=smtp.gmail.com
GOTRUE_SMTP_PORT=465
GOTRUE_SMTP_USER=email_sender@some_company.com
GOTRUE_SMTP_PASS=email_sender_password
GOTRUE_SMTP_ADMIN_EMAIL=comp_admin@some_company.com
#SMTP系の設定もできるだけ変えておいた方がいいのは間違いないです。
# This user will be created when AppFlowy Cloud starts successfully
# You can use this user to login to the admin panel
GOTRUE_ADMIN_EMAIL=admin@example.com
GOTRUE_ADMIN_PASSWORD=password
#ここも変えます。gotrueのadminパネルのユーザなので、メルアド・パスワードともに強固にしておく必要があります。
# File Storage
# Create the bucket if not exists on AppFlowy Cloud start up.
# Set this to false if the bucket has been created externally.
APPFLOWY_S3_CREATE_BUCKET=true
# This is where storage like images, files, etc. will be stored.
# By default, Minio is used as the default file storage which uses host's file system.
# Keep this as true if you are using other S3 compatible storage provider other than AWS.
APPFLOWY_S3_USE_MINIO=true
APPFLOWY_S3_MINIO_URL=http://minio:9000 # change this if you are using a different address for minio
APPFLOWY_S3_ACCESS_KEY=minioadmin
APPFLOWY_S3_SECRET_KEY=minioadmin
APPFLOWY_S3_BUCKET=appflowy
#APPFLOWY_S3_REGION=us-east-1
#ここのSecretKey系も変更:
# PgAdmin
# Optional module to manage the postgres database
# You can access the pgadmin at http://your-host/pgadmin
# Refer to the APPFLOWY_DATABASE_URL for password when connecting to the database
PGADMIN_DEFAULT_EMAIL=admin@example.com
PGADMIN_DEFAULT_PASSWORD=password
# パスワード変更のほか、docker-composeを編集し、pgadminごと停止してしまうのも手と思います。(私はそっちです)
# Portainer (username: admin)
PORTAINER_PASSWORD=password1234
# pgadminと同様、Portainerごと停止がよいかもしれません。
ついでに、少なくとも動作確認の間はユーザ追加を無効にするべく、ドキュメントに従ってユーザ追加を無効にしておきます。(GOTRUE_ADMIN_EMAIL、GOTRUE_ADMIN_PASSWORDで設定したユーザ、パスワードで入ることができるので、その点は問題ないです。)
- GOTRUE_DISABLE_SIGNUP=true
さて、設定ファイルを弄ったら、いったんdocker volume類を削除しておきます(でないと脆弱なパスワード・ユーザが残存していてうまく起動しなくなる。)
Dockerのネットワーク設定をipv6に変更
ここでDockerのネットワークをipv6対応にしておきます。
Dockerのネットワークをipv6対応にしておかないと、ipv6のEC2インスタンスから、OAuthのトークン交換するときの接続に失敗することになります。
さてまずはdocker-compose.ymlを編集してdocker-composeで立ち上がるコンテナ群のデフォルトネットワークでipv6を有効にしておきます。
ipv6 CIDRにはVPCの余っているCIDR範囲を適当に割り当てます。
(CloudFormationで作った場合VPCのCIDRが余っているはずなので、それを割り当てます)
networks:
default:
enable_ipv6: true
ipam:
config:
- subnet: <適当なipv6 CIDR>
ここまでで、コンテナにipv6のアドレスが割り当てられるようになるのですが、この状態だと外からコンテナへのアクセス(とその戻り)通信は可能なのですが、コンテナから外へのアクセスができません。
普通にNginxなどを稼働する限りではあまり困らないのですが、ことAppFlowyについてはOAuthのトークン交換の際にアウトバウンド通信が必要なので、この設定だと認証ができなくて詰みます。
めちゃくちゃ苦労して(マジで5時間とか溶かした)見つけた結論としては、Dockerデフォルトではiptablesがipv4の方にしか入っていません。(ip6tablesが入っていません)
この状態でもインバウンド通信については、docker-proxyがいい感じに処理しているようなのですが、アウトバウンド通信についてはipv4が優先解決される挙動となってしまっていました。(このいい感じの処理の全貌が分からなくて非常に時間がかかった。)
ipv4が優先解決されると、Public IPv4アドレスを持たないインスタンスからは外に出ることができず、通信できなくなっています。
そこで、docker-daemonの設定を変更して、ip6tablesの設定もdocker-daemonが変更できるようにしてあげます。experimentalですが、多分これが一番簡単な解決法なはず、、、
{
"experimental": true,
"ip6tables": true
}
そうしたら、docker daemonをsystemctlなどでリスタートします。
これで、ipv6でアウトバウンド通信できるようになっているはず。
例えば以下のようなもので確認可能です。設定がちゃんと入っていないと応答なしになります。
(appflowyが立ち上がっている状態で)
docker-compose exec nginx bash
<!-- 以下 nginxコンテナ内 -->
curl https://google.co.jp
GCP(認証用)の準備
Appflowyをセルフホストする場合、認証系には外部IdPが使えます。
SAMLにも対応はしているのですがちょっと手軽にやろうとすると、GCP/Github/DiscordでのOAuthからの選択になります。
今回はGCPを選択して進めます。GCP以外で言うと多分Discordはipv6に対応している(AAAAを引ける)ので利用できるかもしれません。Githubはipv6では無理です。
さて、以下設定です。
GCPのプロジェクトとアカウントを作成したのち、OAuth Client IDを作成します。
GCPプロジェクトの同意画面を作成していなかったので、新規に設定します。(よくある、アプリhogeはfugaの許可をリクエストしていますみたいなやつ)
ユーザはExternal(Internalでもいいのですが、CloudIdentity組織を作っていなかったので、Externalのテストモードで運用するつもり)、
スコープは特に追加しません。(ログイン用途だけなので、GCP側のスコープは不要)
証明書の準備が全項で終わっているので、証明書に使うドメインでOAuth認証情報を登録します。
これ以降は、公式のドキュメント(ただしまだGithubにしか無くて、サイトで公開はされていない?)
に沿えば問題ありません。
また、APIの同意画面やらなんやらは以下も参考になります。
AppFlowy起動と動作確認
さて、ようやっと設定が終わったのでAppflowyを再度立ち上げます。
そしたら、アプリから使えるようにアプリ側の設定を変更します。
とはいっても、このドキュメントの通りで、接続先のサーバを自前のサーバにするだけです。
繋がったら、設定したGoogleのOAuthでログインして、、、
いけました!
オチ:Googleドキュメントでよかった。
この検証と並行して、Notionが使えなくて困っていたため、Googleドキュメントを使っていたのですが、、、
Googleドキュメントでよくね?ポイント
- 最近はNotionっぽいページ区切り無し表示ができる
- マークダウンが使える!(コードとかは使えないが超メインどころは使える)
- サブページも行けるしページアイコンも足せる!
ということで、Googleドキュメントで要件を満たすため、セルフホストAppFlowy君はお蔵入りになりました。ぶっちゃけセキュリティとか証明書管理とか面倒なので、、、
まぁ、ipv6 Dockerに詳しくなれたからいいか、、、という気持ち。
最後に
Appflowyに限らずIpv6で何かをセルフホストしたい方は私の屍を越えて行ってもらえると早いかなと思います。
Discussion