CodeBuildのbuildspec.ymlの書き方を覚えて、Laravelを立ち上げよう!

technologies

おはようございます!

ベンジャミンの木村です!

みなさま、ECSでコンテナを立てられていますか?私は最初すごく苦労しました。

パイプラインを回すたび、CodeBuildが止まり、ようやくビルドができたと思ったところで、デプロイ時にコンテナが一瞬で落ちる。。。そんな経験、誰もがしてきたのではないでしょうか?

今回、せめてCodeBuildのビルドのところでつまずかないように、弊社のbuildspec.ymlの書き方をご紹介します。

※今回、Dockerfileの記載は載せておりません。Dockerfileの準備に関しましては、以下記事が大変参考になりましたので、こちらをご参照ください。

Docker+LEMP環境構築

https://hanlabo.co.jp/memorandum/1857

【超入門】20分でLaravel開発環境を爆速構築するDockerハンズオン

https://qiita.com/ucan-lab/items/56c9dc3cf2e6762672f4

目次

構成図

今回はこの赤枠の部分を説明していきます。

ファイル構成

.
├── appspec.yml # デプロイの設定ファイル
├── buildspec.yml # 今回解説するbuildspec.yml
├── docker-compose.yml
├── infra
│   ├── mysql
│   │   ├── Dockerfile # dbコンテナ用のDockerfile※ローカル用です(AWSではAuroraがあるので使いません)
│   │   └── my.cnf
│   ├── nginx
│   │   ├── Dockerfile # nginxコンテナ用のDockerfile
│   │   └── default.conf
│   └── php # phpの設定ファイル集
│       ├── Dockerfile # phpコンテナ用のDockerfile
│       └── conf
│           ├── php-fpm.conf
│           ├── php.ini
│           └── www.conf
├── src # Laravelのsrcディレクトリ
│   ├── README.md
│   ├── app
│   …………
└── taskdef.json # ECSを立ち上げるための指示書

buildspec.ymlとは?

簡単に説明しますと、ビルドの指示書のようなものです。

書かれていることは以下の通りです。

  • ビルドを行うための準備
  • ビルド(コンテナイメージの構築)
  • ビルド後に行う作業(イメージのpushやタスクの定義など)

buildspec.ymlに書かれていることが全て実行されると、ECRにコンテナイメージが挿入され、いよいよコンテナが立ち上がる準備が整ったという状態になります。

buildspec.ymlの全体を見てみよう

まずはLaravelのコンテナを立ち上げるためのbuildspec.ymlの全体像を下記に記載します。

初見でこんなコード見せられても訳がわからないよ!』という気持ちはわかります。ですので、この後、まずはbuildspec.ymlに記載されている各セクションフェーズというものについて解説させていただきます。

version: 0.2

phases:
  install:
    runtime-version:
      docker: 20
  pre_build:
    commands:
      # # ECRリポジトリのURIを環境変数に設定
      - REPO_URI_NGINX="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/nginx"
      - REPO_URI_PHPFPM="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/php-fpm"

      # # タグ名にgitのコミットハッシュを利用
      - IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | cut -c 1-7)
      - TAG="${ENV}-${IMAGE_TAG}"

      # # aws cliの設定
      - aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com

      # # nginxの設定ファイルの変更
      - sed -i -e "s#fastcgi_pass php:9000#fastcgi_pass localhost:9000#" ./infra/nginx/default.conf
      
  build:
    commands:
      # # docker buildを実行
      - docker build -f ./infra/nginx/Dockerfile -t src_nginx:latest .
      - docker build -f ./infra/php/Dockerfile -t src_php:latest .

      # # doeckr imageのタグ付け
      - docker tag src_nginx:latest ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
      - docker tag src_php:latest ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}
      - docker images
    
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...

      # # dockr pushを実行
      - docker push ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
      - docker push ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}

      # # taskdef.jsonの変数の変更
      - sed -i -e "s#<AWS_ACCOUNT_ID>#${AWS_ACCOUNT_ID}#" taskdef.json
      - sed -i -e "s#<REGION>#${REGION}#" taskdef.json
      - sed -i -e "s#<SERVICE>#${SERVICE}#" taskdef.json
      - sed -i -e "s#<ENV>#${ENV}#" taskdef.json
      - sed -i -e "s#<TAG>#${TAG}#" taskdef.json
      - sed -i -e "s#<TASK_CPU>#${TASK_CPU}#" taskdef.json
      - sed -i -e "s#<TASK_MEMORY>#${TASK_MEMORY}#" taskdef.json
      - cat taskdef.json

artifacts:
  files:
    - appspec.yml
    - taskdef.json

※Laravelに必要なパッケージなどをインストールするためのコードは、Dockerfileに記載している想定でbuildspec.ymlを記載しております。

セクションとは?

セクションとは、特定の設定やデータをグループ化するためのブロックのことを指します。先ほどのコードで言うところの、version、phases、reports、artifactsがそれに当たります。

  • version
    • version: ビルド仕様のバージョンを指定します
  • phases
    • ビルドのプロセスの各段階を定義するメインセクションです。通常、installpre_buildbuildpost_buildのビルドの段階的なプロセスが含まれます
  • artifacts
    • ビルド成果物である、『デプロイに必要なファイル(appspec.yml)』や『ECSのタスク定義を記載するファイル(taskdef.json)』を指定するためのセクションです。これにより、ビルド成果物を保存し、後続のデプロイプロセスなどで使用することができます

フェーズ(phases)とは?

『buildspec.ymlにはセクションというブロックがあるんだ〜』とご理解いただけましたか?次に、メインセクションのフェーズ(phases)について説明させていただきます。

フェーズは、ビルドプロセスを段階的に進行するためのステップを定義するためのもので、それぞれのフェーズには特定の役割があります。先ほどのコードで言うところの、installpre_buildbuildpost_buildがそれに当たります。

以下より、各フェーズで行なっていることを詳細に説明させていただきます。

1. installフェーズの解説

このフェーズでは、ビルドプロセスに必要なツールや依存関係をインストールします。

  install:
    runtime-version:
      docker: 20

ここでは、runtime-versionsセクションを使って、Dockerエンジンのバージョン20を指定しています。これにより、ビルドプロセスが始まる前に、CodeBuildの環境に必要なDockerエンジンがインストールされます。

最新のDockerエンジンのバージョンについては、下記Docker docsの公式ページよりご確認ください。

Docker Engine リリースノート

https://matsuand.github.io/docs.docker.jp.onthefly/engine/release-notes

また、installできるruntime-versionはDockerエンジン以外にも選択可能です。

用途に合わせて、他のものを使用する場合は、下記AWS公式のページをご確認ください。

使用可能なランタイム

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/available-runtimes.html

2. pre_buildフェーズの解説

このフェーズでは、ビルドプロセスが始まる前に必要な設定や準備を行います。

  pre_build:
    commands:
      # # ECRリポジトリのURIを環境変数に設定
      - REPO_URI_NGINX="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/nginx"
      - REPO_URI_PHPFPM="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/php-fpm"

      # # タグ名にgitのコミットハッシュを利用
      - IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | cut -c 1-7)
      - TAG="${ENV}-${IMAGE_TAG}"

      # # aws cliの設定
      - aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com

      # # nginxの設定ファイルの変更
      - sed -i -e "s#fastcgi_pass php:9000#fastcgi_pass localhost:9000#" ./infra/nginx/default.conf

このフェーズでは、ECRリポジトリのURIを環境変数に設定したり、タグ名にgitのコミットハッシュを使用したり、AWS CLIの設定を行ったりします。また、nginxの設定ファイルを変更するためのコマンドも含まれています。

以下に詳細を説明させていだだきます。

  • ECRリポジトリのURIを変数に定義
      - REPO_URI_NGINX="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/nginx"
      - REPO_URI_PHPFPM="${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${ENV}-${SERVICE}-ecr/php-fpm"

ECRリポジトリのURIを変数に定義しています。

ここでいきなり、${AWS_ACCOUNT_ID}${REGION}${ENV}${SERVICE}といった変数が出てきたと思いますが、こちらはどこから登場したのでしょうか?

こちらは、CodeBuild上で定義した環境変数になります。Codebuildでは環境変数を設定することで、それら環境変数をbuildspec.yml上で使用することができます。

設定手順は以下に記載させていただきますので、こちらをご参照ください。

  1. CodePipelineのコンソール画面へ移動する
  2. 「編集」ボタンをクリックする
  3. 「Build」で「ステージを編集する」ボタンをクリックする
  4. ペンマーク(🖊️)をクリック
  5. 開いた画面の下記赤枠部分で環境変数の設定ができる

${TASK_CPU}${TASK_MEMORY}は項番4のpost_buildで使う環境変数になります。

  • タグの定義
      - IMAGE_TAG=$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | cut -c 1-7)
      - TAG="${ENV}-${IMAGE_TAG}"

こちらはECRにつける以下のようなイメージタグをランダムな数値として、変数に定義しています。

ここでもいきなり、${CODEBUILD_RESOLVED_SOURCE_VERSION}という変数が登場しています。こちらも先ほどと同様に、CodeBuild上で設定した環境変数でしょうか?

いいえ、こちらは先ほどとは違い、すでにAWSから用意されたCodeBuild のビルドコマンドで使用できる環境変数になります。

こういった、こちらで用意しなくてもAWSから提供されている環境変数がいくつかございますので、以下AWS公式ページよりご確認ください。

ビルド環境の環境変数

https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/build-env-ref-env-vars.html

  • AWS CLIを使ってECRにログイン
      - aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com

こちらは、ECRにログインするためのコマンドになります。このコマンドを実行しないとECRへイメージをpushやpullするような、動作をすることができません。

  • nginxの設定ファイル(default.conf)の変更
      - sed -i -e "s#fastcgi_pass app:9000#fastcgi_pass localhost:9000#" ./infra/nginx/default.conf

fastcgi_pass app:9000はnginxコンテナからappコンテナに対して9000番portで通信の設定する記述です。(私は初めてコンテナを立てる時、ここで大ハマりしました…

ECSはコンテナ間で通信する際はlocalhostを指定して通信を行うため、記述をapp(コンテナ名)ではなく、localhostに変更する必要があります。上記はsedコマンドを用いて、nginxの設定ファイルから、その記述部分の置換を行なっております。

3. buildフェーズの解説

このフェーズでは、実際にビルドを行い、Dockerイメージを作成します。

      # # docker buildを実行
      - docker build -f ./infra/nginx/Dockerfile -t src_nginx:latest .
      - docker build -f ./infra/php/Dockerfile -t src_php:latest .

      # # doeckr imageのタグ付け
      - docker tag src_nginx:latest ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
      - docker tag src_php:latest ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}
      - docker images

Dockerfileを使ってDockerイメージをビルドし、タグ付けを行います。

以下に詳細を説明させていだだきます。

  • buildの実行
      - docker build -f ./infra/nginx/Dockerfile -t src_nginx:latest .
      - docker build -f ./infra/php/Dockerfile -t src_php:latest .

上記はDockerfileのコマンドを実行し、イメージを作成するためのビルドコマンドになります。

  • コマンドの詳細(nginxのビルドより)
    • docker build:ビルドコマンド
    • -f ./infra/nginx/Dockerfile:Dockerfile の位置するパスを指定します
    • -t src_nginx:latest:イメージにタグを付けます
    • .:Dockerfile内のコマンドを実行するパスを指定しています
      ※こちらの設定をローカルとAWSで合わせるためにdocker-composeではbuildの設定を下記のように記載してください
          build:
      context: .
      dockerfile: ./infra/nginx/Dockerfile

  • doeckr imageのタグ付け
      - docker tag src_nginx:latest ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
      - docker tag src_php:latest ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}

こちらは先ほど作成したlatestとタグのついたイメージ(src_nginx, src_php)を、${ENV}-${IMAGE_TAG}とタグのついた<イメージ名> = <ECRのURI>という形にして、新しいイメージにしています。

src_nginx:latest<ECRのURI>:<タグ>という形にしないと、ECRで使えないからです。

  • docker imagesの確認
      - docker images

最後にdockerイメージがコマンドの実行通りにできているか確認しています。ここまでうまくいっていれば、buildフェーズは完了となります。

[docker imagesの実行ログ]

[Container] 2024/07/19 13:13:18.663560 Running command docker images
REPOSITORY                                                                     TAG              IMAGE ID       CREATED                  SIZE
<AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/dev-verify-kim-ecr/php-fpm   dev-b71e6ed      4f00b703741d   Less than a second ago   482MB
src_php                                                                        latest           4f00b703741d   Less than a second ago   482MB
<AWS_ACCOUNT_ID>.dkr.ecr.ap-northeast-1.amazonaws.com/dev-verify-kim-ecr/nginx     dev-b71e6ed      706f1da034da   About a minute ago       21.8MB
src_nginx                                                                      latest           706f1da034da   About a minute ago       21.8MB
composer                                                                       2.2              e05114c93971   5 weeks ago              196MB
php                                                                            8.1-fpm-buster   f04303983837   13 months ago            357MB
nginx                                                                          1.20-alpine      d6a1e2ab00f7   2 years ago              21.8MB

4. post_buildフェーズの解説

このフェーズでは、ビルドが完了した後に行う作業を定義します。

  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...

      # # dockr pushを実行
      - docker push ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
      - docker push ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}

      # # taskdef.jsonの変数の変更
      - sed -i -e "s#<AWS_ACCOUNT_ID>#${AWS_ACCOUNT_ID}#" taskdef.json
      - sed -i -e "s#<REGION>#${REGION}#" taskdef.json
      - sed -i -e "s#<SERVICE>#${SERVICE}#" taskdef.json
      - sed -i -e "s#<ENV>#${ENV}#" taskdef.json
      - sed -i -e "s#<TAG>#${TAG}#" taskdef.json
      - sed -i -e "s#<TASK_CPU>#${TASK_CPU}#" taskdef.json
      - sed -i -e "s#<TASK_MEMORY>#${TASK_MEMORY}#" taskdef.json

ここでは、ビルドが完了したことを通知し、DockerイメージをECRにプッシュします。また、taskdef.jsonファイルの変数を適切に設定し、最終的にtaskdef.jsonのファイルの中身を表示します。

以下に詳細を説明させていだだきます。

  • イメージのpush
      - docker push ${REPO_URI_NGINX}:${ENV}-${IMAGE_TAG}
      - docker push ${REPO_URI_PHPFPM}:${ENV}-${IMAGE_TAG}

ECRにbuildしたイメージをpushしています。これで、ECRでDockerイメージが使用可能になります。

  • taskdef.jsonの変数を置換
      - sed -i -e "s#<AWS_ACCOUNT_ID>#${AWS_ACCOUNT_ID}#" taskdef.json
      - sed -i -e "s#<REGION>#${REGION}#" taskdef.json
      - sed -i -e "s#<SERVICE>#${SERVICE}#" taskdef.json
      - sed -i -e "s#<ENV>#${ENV}#" taskdef.json
      - sed -i -e "s#<TAG>#${TAG}#" taskdef.json
      - sed -i -e "s#<TASK_CPU>#${TASK_CPU}#" taskdef.json
      - sed -i -e "s#<TASK_MEMORY>#${TASK_MEMORY}#" taskdef.json

下記に示すECSタスクを定義するファイル(taskdef.json)に、いくつか変数を定義しています。Deployする前に、sedコマンドで置換することで、変数に値が入るようにしています。
${TASK_CPU}${TASK_MEMORY}は「2. pre_buildフェーズの解説」で解説したCodeBuild上で定義した環境変数

  • taskdef.json
{
  "containerDefinitions": [
    {
      "name": "nginx",
      "image": "<AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/<ENV>-<SERVICE>-ecr/nginx:<TAG>",
      "cpu": 0,
      "portMappings": [
        {
          "hostPort": 80,
          "protocol": "tcp",
          "containerPort": 80
        }
      ],
      "essential": true,
      "environment": [],
      "mountPoints": [],
      "volumesFrom": [],
      "logConfiguration": {
        "logDriver": "awslogs",
        "secretOptions": null,
        "options": {
          "awslogs-group": "/ecs/<ENV>-<SERVICE>-cluster/nginx",
          "awslogs-region": "<REGION>",
          "awslogs-stream-prefix": "nginx"
        }
      },
      "systemControls": []
    },
    {
      "name": "php-fpm",
      "image": "<AWS_ACCOUNT_ID>.dkr.ecr.<REGION>.amazonaws.com/<ENV>-<SERVICE>-ecr/php-fpm:<TAG>",
      "cpu": 0,
      "portMappings": [
        {
          "name": "php-fpm",
          "hostPort": 9000,
          "protocol": "tcp",
          "containerPort": 9000
        }
      ],
      "essential": true,
      "entryPoint": [],
      "command": [],
      "environment": [],
      "mountPoints": [],
      "volumesFrom": [],
      "secrets": [
        {
          "name": "APP_NAME",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/APP_NAME"
        },
        {
          "name": "APP_ENV",
          "valueFrom": "<ENV>"
        },
        {
          "name": "APP_DEBUG",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/DEBUG"
        },
        {
          "name": "APP_URL",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/APP_URL"
        },
        {
          "name": "LOG_CHANNEL",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/LOG_CHANNEL"
        },
        {
          "name": "LOG_LEVEL",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/LOG_LEVEL"
        },
        {
          "name": "DB_CONNECTION",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_CONNECTION"
        },
        {
          "name": "DB_HOST",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_HOST"
        },
        {
          "name": "DB_PORT",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_PORT"
        },
        {
          "name": "DB_DATABASE",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_DATABASE"
        },
        {
          "name": "DB_USERNAME",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_USERNAME"
        },
        {
          "name": "APP_KEY",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/APP/APP_KEY"
        },
        {
          "name": "DB_PASSWORD",
          "valueFrom": "arn:aws:ssm:<REGION>:<AWS_ACCOUNT_ID>:parameter/<ENV>/<SERVICE>/AURORA/DB_PASSWORD"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "secretOptions": null,
        "options": {
          "awslogs-group": "/ecs/<ENV>-<SERVICE>-cluster/php",
          "awslogs-region": "<REGION>",
          "awslogs-stream-prefix": "php"
        }
      },
      "systemControls": []
    }
  ],
  "family": "<ENV>-<SERVICE>-ecs-taskdef",
  "taskRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/Role-<ENV>-<SERVICE>-ecs-service-cmn-task",
  "executionRoleArn": "arn:aws:iam::<AWS_ACCOUNT_ID>:role/Role-<ENV>-<SERVICE>-ecs-service-cmn-taskexec",
  "networkMode": "awsvpc",
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "cpu": "<TASK_CPU>",
  "memory": "<TASK_MEMORY>",
  "status": "ACTIVE",
  "enableExecuteCommand": true,
  "propagateTags": "SERVICE",
  "runtimePlatform": {
    "cpuArchitecture": "ARM64",
    "operatingSystemFamily": "LINUX"
  },
  "tags": [
    {
      "key": "Name",
      "value": "<ENV>-<SERVICE>-ecs-taskdef"
    },
    {
      "key": "Description",
      "value": "ECS Task Definition"
    },
    {
      "key": "Env",
      "value": "<ENV>"
    },
    {
      "key": "Service",
      "value": "<SERVICE>"
    },
    {
      "key": "Terraform",
      "value": "true"
    }
  ]
}
  • taskdef.jsonの確認
 - cat taskdef.json

最後にtaskdef.jsonが問題なく定義されているかCodeBuildのログから確認します。catで出力した際に、環境変数が置換されていなかったりすると、再びbuildspec.ymlの修正が必要になります。

5. ビルドを実行しよう!

buildspec.ymlが書けたら、パイプラインからビルドを実行してみましょう!

ここまで書ければ、Dockerfileに問題がなければビルドは成功するはずです。パイプラインを回し、下記のようにBuildの部分が緑色になれば成功です!

まとめ

いかがでしたでしょうか?

ECSを立ち上げるのって、本当に難しいですよね。インフラ構築の壁の一つだと思っています。。。

私はbuildspec.ymlの記述でハマりまくったので、今回新たな犠牲者が出ないように今回このブログを記載しました。

このブログを読んでくださいました皆様のお役に立てますと幸いです。

また、buildspec.ymlでのUnitテストの実行方法についても、ブログを記載していますので、Unitテストを実行する際はこちらも併せてご拝見いただけますと幸いです。

Related posts