Terraformを使ってWiki.jsをAmazon RDSをデータベースとして動かしてみた

2024.04.02

こんちには。

データアナリティクス事業本部 機械学習チームの中村(nokomoro3)です。

本記事ではTerraformを使ってWiki.jsをAmazon RDSをデータベースとして動かしてみたいと思います。

Wiki.jsとは

Wiki.jsはNode.js上で動作するJavaScriptで書かれたウィキエンジンです。

イメージとしては以下のような画面となります。

MarkdownやHTMLでウィキを記載することができ、画面上でページの差分が確認できるなど、ドキュメンテーションに必要な機能が豊富です。 ログイン周りや検索周りの統合機能も準備されています。

本記事のようにセルフホスティングも可能となっていますが、ライセンスが「GNU AGPL v3」となっている点はご留意ください。

Terraformの構成

コードは以下に公開しています。

今回は以下のような構成としています。少しStyle Guideからは外れるかもしれませんがご了承ください。

├─environments
│   └─dev/
│          dev.tfvars
│          main.tf
└─modules
    ├─ec2/
    │      config.yml
    │      main.tf
    ├─rds/
    │      main.tf
    └─vpc/
           main.tf

この中でdev.tfvarsだけは自身で設定が必要ですので、以下のような内容を入力してファイルを作成ください。

project_prefix="wikijs"
rds_database="wiki"                # RDSのデータベース名
rds_user="wikijs"                  # RDSのユーザ名
rds_password=""                    # RDSのパスワードを入力
wikijs_allow_ingress_cidr_block="" # Wiki.jsへのアクセスを許可するPCのIPアドレス範囲を入力

実装の詳細

実装の詳細を説明しつつ、参考にした情報やポイントを紹介します。

environments/dev/main.tf

mainのtfファイルです。

こちらでは特に何もしておらず、子モジュールを呼び出している形となります。

variable "project_prefix" {}
variable "rds_database" {}
variable "rds_password" {}
variable "rds_user" {}
variable "wikijs_allow_ingress_cidr_block" {}

provider "aws" {
  default_tags {
    tags = {
      project_prefix = var.project_prefix
    }
  }
}

module "vpc" {
  source                   = "../../modules/vpc"
  project_prefix           = var.project_prefix
  allow_ingress_cidr_block = var.wikijs_allow_ingress_cidr_block
}

module "ec2" {
  source                   = "../../modules/ec2"
  project_prefix           = var.project_prefix
  public_subnet_id         = module.vpc.subnet_id
  public_security_group_id = module.vpc.security_group_id
  rds_host                 = module.rds.rds_host
  rds_database             = var.rds_database
  rds_password             = var.rds_password
  rds_user                 = var.rds_user
}

module "rds" {
  source                    = "../../modules/rds"
  project_prefix            = var.project_prefix
  private_subnet_ids        = module.vpc.private_subnet_ids
  private_security_group_id = module.vpc.private_security_group_id
  rds_password              = var.rds_password
  rds_database              = var.rds_database
  rds_user                  = var.rds_user

}

modules/vpc/main.tf

VPCのモジュールです。

variable "project_prefix" {}
variable "allow_ingress_cidr_block" {}

output "subnet_id" {
  value = aws_subnet.public_1a.id
}

output "security_group_id" {
  value = aws_security_group.sg.id
}

output "private_subnet_ids" {
  value = [
    aws_subnet.private_1a.id,
    aws_subnet.private_1c.id
  ]
}

output "private_security_group_id" {
  value = aws_security_group.private_sg.id
}

// VPC
resource "aws_vpc" "vpc" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = var.project_prefix
  }
}

// インターネットゲートウェイ作成
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = var.project_prefix
  }
}

// サブネット作成 (パブリック)
resource "aws_subnet" "public_1a" {
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = "10.0.0.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = var.project_prefix
  }
}

// ルートテーブル作成とインターネットゲートウェイへのルート追加
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = var.project_prefix
  }
}

// ルート
resource "aws_route" "public" {
  route_table_id         = aws_route_table.public.id
  gateway_id             = aws_internet_gateway.igw.id
  destination_cidr_block = "0.0.0.0/0"
}

// サブネットにルートテーブルを関連付け
resource "aws_route_table_association" "route_table_association" {
  subnet_id      = aws_subnet.public_1a.id
  route_table_id = aws_route_table.public.id
}

// セキュリティグループ作成
resource "aws_security_group" "sg" {
  name   = "${var.project_prefix}-sg"
  vpc_id = aws_vpc.vpc.id

  ingress {
    from_port   = 3000
    to_port     = 3000
    protocol    = "tcp"
    cidr_blocks = ["${var.allow_ingress_cidr_block}"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

// サブネット作成 (private)
resource "aws_subnet" "private_1a" {
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = "10.0.1.0/24"
  availability_zone       = "ap-northeast-1a"

  tags = {
    Name = var.project_prefix
  }
}

// サブネット作成 (private)
resource "aws_subnet" "private_1c" {
  vpc_id                  = aws_vpc.vpc.id
  cidr_block              = "10.0.2.0/24"
  availability_zone       = "ap-northeast-1c"

  tags = {
    Name = var.project_prefix
  }
}

// セキュリティグループ作成
resource "aws_security_group" "private_sg" {
  name   = "${var.project_prefix}-private-sg"
  vpc_id = aws_vpc.vpc.id

  ingress {
    from_port   = 5432
    to_port     = 5432
    protocol    = "tcp"
    cidr_blocks = [
      aws_subnet.public_1a.cidr_block
    ]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

VPCを新しく作成し、Publicサブネットを1つ、PrivateサブネットをRDS用に2つのAZで作成しています。

PublicにはWiki.jsが稼働するEC2を配置する予定ですので、このEC2用のSecurityGroupを、開発者マシンのIPアドレス範囲で、3000番のポートでIngressを許可しています。

またPrivateにはRDSを配置する予定ですので、RDS用のSecurityGroupを、Publicサブネットから5432番のポートでIngressを許可しています。

modules/rds/main.tf

RDSのモジュールです。

variable "project_prefix" {}
variable "private_subnet_ids" {}
variable "private_security_group_id" {}
variable "rds_database" {}
variable "rds_user" {}
variable "rds_password" {}

output "rds_host" {
  value = aws_db_instance.wikijs.address
}

resource "aws_db_subnet_group" "wikijs" {
  name       = var.project_prefix
  subnet_ids = var.private_subnet_ids
}

resource "aws_db_parameter_group" "wikijs" {
  name   = var.project_prefix
  family = "postgres15"
}

resource "aws_db_instance" "wikijs" {
  instance_class         = "db.t3.micro"
  engine                 = "postgres"
  engine_version         = "15.5"
  storage_type           = "gp2"
  allocated_storage      = 20
  db_name                = var.rds_database
  username               = var.rds_user
  password               = var.rds_password
  parameter_group_name   = aws_db_parameter_group.wikijs.name
  identifier             = var.rds_user
  vpc_security_group_ids = [var.private_security_group_id]
  db_subnet_group_name   = aws_db_subnet_group.wikijs.name
  skip_final_snapshot    = true
  deletion_protection    = true

  lifecycle {
    prevent_destroy = false
  }
}

特筆すべき点はそこまでないのですが、接続情報はEC2と共通で参照するためvariableにしています。

また削除保護が設定されているため、terraform destroyのときに消えないようになっています。

modules/ec2/main.tf

Wiki.jsが稼働するEC2のモジュールです。

variable "project_prefix" {}
variable "public_subnet_id" {}
variable "public_security_group_id" {}
variable "rds_host" {}
variable "rds_database" {}
variable "rds_user" {}
variable "rds_password" {}

output "config_content" {
  value = local.config_content
}

output "user_data" {
  value = aws_instance.main.user_data_base64
}

# 信頼ポリシー
data "aws_iam_policy_document" "ec2" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["ec2.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

# IAMロール
resource "aws_iam_role" "role" {
  name               = "${var.project_prefix}-instance-role"
  assume_role_policy = data.aws_iam_policy_document.ec2.json
  managed_policy_arns = [
    "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
  ]
}

# インスタンスプロファイル
resource "aws_iam_instance_profile" "instance_profile" {
  name = "${var.project_prefix}-instance-role-profile"
  role = aws_iam_role.role.name
}

data "local_file" "config_file" {
  filename = "${path.module}/config.yml"
}

locals {
  config_content = data.local_file.config_file.content
}

# EC2インスタンス
resource "aws_instance" "main" {
  ami                         = "ami-031134f7a79b6e424"
  instance_type               = "t3.small"
  subnet_id                   = var.public_subnet_id
  iam_instance_profile        = aws_iam_instance_profile.instance_profile.name
  vpc_security_group_ids      = [var.public_security_group_id]
  associate_public_ip_address = true

  user_data = <<-EOF
              #!/bin/bash

              # nodeのインストール
              yum install nodejs npm -y

              # wikijsのインストール
              mkdir -p /opt/wikijs
              cd /opt/wikijs
              wget https://github.com/Requarks/wiki/releases/latest/download/wiki-js.tar.gz
              mkdir wiki
              tar xzf wiki-js.tar.gz -C ./wiki
              cd ./wiki

              # RDSの証明書バンドルの取得
              wget https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem

              # configファイルを実体化
              echo "${local.config_content}" > config.yml

              # 環境変数
              export DB_TYPE="postgres"
              export DB_HOST="${var.rds_host}"
              export DB_NAME="${var.rds_database}"
              export DB_PORT=5432
              export DB_USER="${var.rds_user}"
              export DB_PASS="${var.rds_password}"

              # logrotateの設定
              mkdir -p /var/log/wikijs/
              echo "/var/log/wikijs/*.txt {
                daily
                rotate 7
                compress
                delaycompress
                missingok
                notifempty
                create 640 root adm
              }" >/etc/logrotate.d/wikijs

              # wikijsの起動
              node server 2>&1 | tee /var/log/wikijs/log.txt &

              EOF

  tags = {
    Name = "${var.project_prefix}"
  }
}

必要なセットアップはユーザデータで行っています。

Wiki.js向けのインストール手順は以下を参考にしています。

RDSの証明書バンドルの取得がポイントで、こちらをWiki.jsの設定として読み込む必要があります。

これをしない場合、Wiki.jsからSSLでRDSに接続時する際にエラーとなります。

Wiki.jsの設定ファイルであるconfig.ymlはローカルに保存しておき、そちらをそのままユーザデータを使ってアップロードしています。 必要な接続情報はEC2の環境変数で与えられるように対応しています。config.ymlの詳細は後述します。

接続情報はterraformのvariableから環境変数に取得しており、本来はSecret Manager等をつかうべきかもしれませんが、今回は簡単に構築しています。

modules/ec2/config.yml

最後にWiki.jsの設定ファイルであるconfig.ymlの説明をします。

以下は必要箇所のみ抜粋しています。

######################################################################
# Wiki.js - CONFIGURATION                                             #
#######################################################################
# Full documentation + examples:
# https://docs.requarks.io/install

# ---------------------------------------------------------------------
# Port the server should listen to
# ---------------------------------------------------------------------

port: 3000

# ---------------------------------------------------------------------
# Database
# ---------------------------------------------------------------------
# Supported Database Engines:
# - postgres = PostgreSQL 9.5 or later
# - mysql = MySQL 8.0 or later (5.7.8 partially supported, refer to docs)
# - mariadb = MariaDB 10.2.7 or later
# - mssql = MS SQL Server 2012 or later
# - sqlite = SQLite 3.9 or later

db:
  type: \$(DB_TYPE)
  host: '\$(DB_HOST)'
  port: \$(DB_PORT)
  db: '\$(DB_NAME)'
  user: '\$(DB_USER)'
  pass: '\$(DB_PASS)'
  ssl: true

  # Optional - PostgreSQL / MySQL / MariaDB only:
  # -> Uncomment lines you need below and set `auto` to false
  # -> Full list of accepted options: https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options
  sslOptions:
    auto: false
    # rejectUnauthorized: false
    ca: /opt/wikijs/wiki/global-bundle.pem
    # cert: path/to/cert.crt
    # key: path/to/key.pem
    # pfx: path/to/cert.pfx
    # passphrase: xyz123

  # Optional - PostgreSQL only:
  schema: public

  # SQLite only:
  storage: path/to/database.sqlite

#######################################################################
# ADVANCED OPTIONS                                                    #
#######################################################################
# Do not change unless you know what you are doing!

# --- 以降省略 ---

db:ブロックの設定は環境変数を参照するようにしていますが、Terraformで$が解釈されないようにエスケープをしています。

またsslOptions:ブロックのca:で先ほどEC2のユーザデータで取得する証明書バンドルのパスを設定しています。

これとは別にssl:ブロックがconfig.ymlには存在するのですが、こちらはWiki.js側のWebサーバーとしてのSSL設定なので混同されないようにされてください。

config.ymlの詳細は以下にも記載されています。

デプロイ

environments/devでコマンドを実行していきます。

initをまず実行します。

terraform init

次のapplyを実行すればOKです。

terraform apply -var-file dev.tfvars

動作確認

マネジメントコンソールからEC2インスタンスのPublic IPを確認します。

こちらに基づいて、`http://{確認したIPアドレス}:3000`にアクセスすると以下のような管理者アカウント作成画面へ遷移しますので、アドレスやパスワードを設定します。

次にログイン画面に遷移しますので、ログインします。

最初に何をするかの画面に遷移しますので、今回は「CREATE HOME PAGE」を選択します。

「Markdown」を選択します。

ページタイトルとページ説明を記載します。

内容を記載し、右上の「CREATE」を押下します。

すると以下のような画面が確認できます。

注意点

デフォルトでは未ログインユーザに対しても閲覧権限がある状態となりますので、問題がある場合は管理者画面からGuests Groupのread:pages権限のチェックを外してご利用ください。

後片付け

以下でリソースを削除します。

terraform destroy -var-file dev.tfvars

RDSはマネジメントコンソールの方から削除されてください。

まとめ

如何でしたでしょうか。本記事がWiki.jsを使用される方の参考になれば幸いです。

参考