先日、社内の技術共有会でTerraformを紹介しました。
もともと社内では AWS CDK の活用が盛んなため、それに便乗して別のIaCアプローチとして紹介しました。
今回は、Terraformを利用してWebアプリケーションを構築する方法を記事にまとめました。
説明はざっくりとしつつですが、かなりボリュームある内容なので、5回に分けて紹介することにします。
スタック
- php 8系
- laravel 10
- Nginx 1.25
- Node.js v18
- React/Vite
- Terraform 1.5.0
各技術の詳細な説明は省きますが、なるべく要点をまとめて紹介できればと思います。
課金に不安がある方
この記事では、AWSの有料リソースを使用します。
不安な方は、あらかじめAWS Budgetsで監視設定をするのが良いでしょう。
目次
- 環境構築 ← ココ
- Terraformでインフラ構築.ver1
- Terraformでインフラ構築.ver2
- Terraformでインフラ構築.ver3
- おまけ
1.環境構築
インフラを構築する前に、
- ドメインの取得
- デプロイするアプリケーションを構築
- Terraform環境構築
を済ませておきます。
1-1. ドメインの取得
アプリケーションのドメインを取得します。
AWS Route53からドメインを取得できます。
ドメインの取得する方法はこちらの記事を参考にすると良いでしょう。
1-2. デプロイするアプリケーションを構築
アプリケーションを構築します。
今回はバックエンドAPIにLaravel、フロントエンドにReactを利用します。
githubでリポジトリを作成し、VSCodeのDevContainerを利用して開発環境構築をしましょう。
githubでリポジトリ作成する方法はこちらを参考に。
DevContainerを知りたい方はこちらを参考に。
githubでリポジトリを作成した後、ローカル環境にcloneします。
git clone git@github.com:[アカウント名]/[リポジトリ名].git
cloneしたリポジトリをVSCodeで開きます。
以下は構成になります。
構成
.
├── devcontainer.json
├── docker
│ ├── app
│ │ ├── Dockerfile
│ │ ├── php-fpm.d
│ │ │ └── zzz-www.conf
│ │ └── requirements.txt
│ ├── db
│ └── nginx
│ ├── Dockerfile
│ └── local.conf
└── docker-compose.yml
nginx,php,node.js,mariaDBの開発環境構築を行います。
DevContainerでは.devcontainer
以下のdevcontainer.json
を参照し、構築する環境の初期構成を確認します。dockerComposeFile
のプロパティにdocker-compose.yml
を指定することで、同階層のdocker-compose.yml
で定義したインフラを立ち上げます。
{
"name": "TF_LARA_APP",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"features": {},
"customizations": {
"vscode": {
"extensions": [
"esbenp.prettier-vscode",
"adpyke.vscode-sql-formatter",
"bierner.markdown-mermaid",
"bmewburn.vscode-intelephense-client",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"felixfbecker.php-debug",
"kumar-harsh.graphql-for-vscode",
"rexshi.phpdoc-comment-vscode-plugin",
"valeryanm.vscode-phpsab",
"vscodeshift.mui-snippets",
"mhutchie.git-graph",
"donjayamanne.githistory",
"shufo.vscode-blade-formatter",
"EditorConfig.EditorConfig",
"open-southeners.laravel-pint"
],
"settings": {
"editor.formatOnSave": true,
"terraform.languageServer.ignoreSingleFileWarning": true,
"phpsab.autoRulesetSearch": false,
"phpsab.standard": "/workspace/phpcs.xml",
"phpsab.executablePathCS": "/home/vscode/.composer/vendor/bin/phpcs",
"phpsab.executablePathCBF": "/home/vscode/.composer/vendor/bin/phpcbf",
"phpsab.snifferMode": "onSave",
"laravel-pint.enable": true,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[php]": {
"editor.defaultFormatter": "open-southeners.laravel-pint",
"editor.formatOnSave": true
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
"[scss]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
}
},
}
},
"forwardPorts": [
80,
443
],
"postCreateCommand": "bash scripts/install-dev-tools.sh",
"remoteUser": "vscode"
}
version: "3"
services:
app:
build:
context: ./docker/app
args:
# [Option] Install Node.js
INSTALL_NODE: "true"
# fermium = v14
# gallium = v16
NODE_VERSION: "lts/hydrogen"
volumes:
- ~/.aws:/home/vscode/.aws:cached
- ~/.ssh:/home/vscode/.ssh:cached
- ../:/workspace:cached
- /var/run/docker.sock:/var/run/docker.sock
- php-fpm-socket:/var/run/php-fpm
- frontend-node-modules:/workspace/frontend/node_modules
- backend-node-modules:/workspace/backend/node_modules
- backend-vendor:/workspace/backend/vendor
environment:
NODE_OPTIONS: "--max-old-space-size=16777" # heap size 16GB
web:
build:
context: ./docker/nginx
ports:
- 80:80
- 443:443
- 8080:8080
- 8000:8000
working_dir: /workspace
volumes:
- ../:/workspace:cached
- ./docker/nginx/local.conf:/etc/nginx/conf.d/default.conf
- php-fpm-socket:/var/run/php-fpm
- backend-vendor:/workspace/backend/vendor
environment:
DB_CONNECTION: mysql
DB_HOST: db
DB_DATABASE: test
DB_USERNAME: mariadb
DB_PASSWORD: mariadb
db:
image: mariadb:10.4
ports:
- 13306:3306
volumes:
- mariadb-data:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: mariadb
MYSQL_DATABASE: test
MYSQL_USER: mariadb
MYSQL_PASSWORD: mariadb
volumes:
mariadb-data: null
php-fpm-socket: null
frontend-node-modules: null
backend-node-modules: null
backend-vendor: null
servicesは、app,web,dbとしました。
それぞれのserviceで定義するコンテナ情報は同階層のdocker/
で定義します。
各サービスでcontext: ./docker/nginx
のように定義することで、それぞれのコンテナ定義(Dockerfile)を参照し立ち上げます。
#-------------------------------------------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information.
#-------------------------------------------------------------------------------------------------------------
FROM php:8.1.18-fpm-bullseye
# Avoid warnings by switching to noninteractive
ENV DEBIAN_FRONTEND=noninteractive
# This Dockerfile adds a non-root user with sudo access. Use the "remoteUser"
# property in devcontainer.json to use it. On Linux, the container user's GID/UIDs
# will be updated to match your local UID/GID (when using the dockerFile property).
# See https://aka.ms/vscode-remote/containers/non-root-user for details.
ARG USERNAME=vscode
ARG USER_UID=1000
ARG USER_GID=$USER_UID
RUN apt-get update \
&& apt-get -y install --no-install-recommends apt-utils dialog 2>&1 \
# install git iproute2, procps, lsb-release (useful for CLI installs)
&& apt-get -y install --no-install-recommends \
git openssh-client less iproute2 procps iproute2 lsb-release zip unzip \
jq vim locales default-mysql-client rsync groff-base \
python3-pip python3-dev python3-setuptools \
# require generate pdf
libjpeg62-turbo xfonts-75dpi fontconfig libx11-6 libxcb1 libxext6 libxrender1 \
xfonts-base libfontconfig1 fontconfig-config libjpeg62-turbo libxdmcp6 libxau6 \
libx11-data ucf fonts-dejavu-core sensible-utils fonts-noto-cjk \
# require npm install.. dart-sass
python2 \
# require docker
ca-certificates gnupg \
#
# locale ja_JP.UTF-8
&& sed -i -E 's/# (ja_JP.UTF-8)/\1/' /etc/locale.gen \
&& locale-gen \
&& update-locale LANG=ja_JP.UTF-8 \
&& ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime \
&& echo 'Asia/Tokyo' > /etc/timezone \
#
# php.ini
&& mv /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini \
&& sed -i -E 's/^;?date.timezone =.*/date.timezone = Asia\/Tokyo/' /usr/local/etc/php/php.ini \
&& { \
echo "memory_limit = 512M"; \
echo "post_max_size = 300M"; \
echo "upload_max_filesize = 500M"; \
} > /usr/local/etc/php/conf.d/app.ini \
#
# Install xdebug
&& yes | pecl install xdebug-3.1.6 \
&& { \
echo "zend_extension=$(find /usr/local/lib/php/extensions/ -name xdebug.so)"; \
echo "xdebug.log=/tmp/xdebug.log"; \
echo "xdebug.mode=develop, debug"; \
echo "xdebug.start_with_request=yes"; \
echo "xdebug.client_host=host.docker.internal"; \
echo "xdebug.client_port=9003"; \
} > /usr/local/etc/php/conf.d/xdebug.ini \
&& touch /tmp/xdebug.log \
&& chmod 666 /tmp/xdebug.log \
#
&& apt-get -y install libonig-dev \
# Docker
&& curl -fsSL https://get.docker.com -o get-docker.sh && sh ./get-docker.sh && rm -f get-docker.sh \
#
# Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user.
&& groupadd --gid $USER_GID $USERNAME \
&& useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \
# [Optional] Add sudo support for the non-root user
&& apt-get install -y sudo \
&& echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\
&& chmod 0440 /etc/sudoers.d/$USERNAME \
#
# Clean up
&& apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/* \
&& docker-php-ext-install opcache pdo_mysql bcmath \
&& yes | pecl install apcu \
&& docker-php-ext-enable apcu \
&& python3 -m pip install --upgrade pip \
&& pip3 install openpyxl jq wheel \
&& python3 -m pip install --upgrade Pillow
RUN if [ "aarch64" = $(uname -m) ]; then \
curl -sSL https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.bullseye_arm64.deb -o wkhtmlto.deb; \
dpkg -i wkhtmlto.deb; \
rm -rf wkhtmlto.deb; \
fi
# Switch back to dialog for any ad-hoc use of apt-get
ENV DEBIAN_FRONTEND=dialog
# [Optional] Install a version of Node.js using nvm for front end dev
ARG INSTALL_NODE="true"
ARG NODE_VERSION="lts/*"
RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "umask 0002 && curl -sS -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash && . ~/.nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# php-fpm.sock
RUN mkdir -p /var/run/php-fpm
COPY ./php-fpm.d/zzz-www.conf /usr/local/etc/php-fpm.d/zzz-www.conf
# composer command
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# .bash_aliases
RUN { \
echo "export LANG=ja_JP.UTF-8"; \
echo "alias ll='ls -al'"; \
echo "alias la='ls -A'"; \
echo "alias l='ls -CF'"; \
echo "alias ti='terraform init'"; \
echo "alias tf='terraform fmt'"; \
echo "alias tp='terraform plan'"; \
echo "alias to='terraform output'"; \
echo "alias ta='terraform apply'"; \
echo "alias taf='terraform apply -auto-approve'"; \
echo "alias tfu='terraform force-unlock'"; \
} > /home/vscode/.bash_aliases
# tfenv install
RUN su "vscode" -c "git clone https://github.com/tfutils/tfenv.git ~/.tfenv" 2>&1 \
&& echo 'export PATH="$PATH:~/.tfenv/bin"' >> "/home/vscode/.bashrc" \
&& su "vscode" -c '~/.tfenv/bin/tfenv install 1.5.0 && ~/.tfenv/bin/tfenv use 1.5.0'
# AWS CLI v2 install
RUN curl -sS "https://awscli.amazonaws.com/awscli-exe-linux-$(uname -m).zip" -o "awscliv2.zip" \
&& unzip awscliv2.zip \
&& ./aws/install \
&& rm -rf aws awscliv2.zip
# Install ecspresso
RUN curl -Lo ecspresso.tar.gz "https://github.com/kayac/ecspresso/releases/download/v2.5.0/ecspresso_2.5.0_linux_amd64.tar.gz" \
&& tar -xzf ecspresso.tar.gz \
&& chmod +x ecspresso \
&& mv ecspresso /usr/local/bin/ \
&& rm ecspresso.tar.gz
# [Optional] Uncomment this line to install global node packages.
RUN su vscode -c "source ~/.nvm/nvm.sh && npm install -g npm-check-updates depcheck aws-cdk@^2" 2>&1
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
COPY ./requirements.txt /tmp/pip-tmp/
RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
&& rm -rf /tmp/pip-tmp
[www]
listen = /var/run/php-fpm/php-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0666
php_flag[expose_php] = off
pm.max_children = 10
aws-mfa
boto3
FROM nginx:alpine
log_format combined_backend 'backend $remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent $request_time"';
access_log /dev/stdout combined;
error_log /dev/stderr warn;
server_tokens off;
proxy_buffer_size 32k;
proxy_buffers 50 32k;
proxy_busy_buffers_size 64k;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
server_name localhost;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
open_file_cache max=100000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
client_max_body_size 300m;
listen 80;
# listen 443 ssl;
root /workspace/backend/public;
access_log /dev/stdout combined_backend;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-XSS-Protection "1; mode=block";
add_header X-Content-Type-Options "nosniff";
index index.php index.html index.htm;
charset utf-8;
# error_page 404 /index.php;
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
location /images/ { access_log off; }
location /fonts/ { access_log off; }
location /css/ { access_log off; }
location /js/ { access_log off; }
location ~ /\.(?!well-known).* {
deny all;
}
location /storage/ {
root /workspace/backend/storage/app;
}
location /login {
try_files $uri $uri/ /index.php?$query_string;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php-fpm/php-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
server {
listen 80 default_server;
server_name _;
location = /healthcheck {
return 200;
access_log off;
break;
}
location / {
return 403;
}
}
Laravelをインストール
今回はバックエンド、フロントエンドのソースを分ける構成にしています。
トップディレクトリで
- /backend
- /frontend
をそれぞれ作成し、/backendにlaravelをインストールします。
(devcontainerを立ち上げた時、各ディレクトリがマウントされている場合はlaravel/reactがインストールできないので、その場合は、docker-compose.ymlのvolumesを一度コメントアウトしてから立ち上げてインストールしてください)
$ mkdir backend
$ cd backend
$ composer create-project laravel/laravel . "^10.0"
nginxではlocalhost:80のリクエストをbackend/public/index.phpで指定しています。
したがって、localhost
をブラウザで開くとLaravelの初期画面が表示されていればOKです。
LaravelはあくまでバックエンドAPIとして使用する想定なので、以下のように修正してjsonが返るようにしましょう。
/**
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
$this->routes(function () {
Route::middleware('api') // prefix消す
->group(base_path('routes/api.php'));
// Route::middleware('web')
// ->group(base_path('routes/web.php'));
});
}
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| API Routes
|--------------------------------------------------------------------------
|
| Here is where you can register API routes for your application. These
| routes are loaded by the RouteServiceProvider and all of them will
| be assigned to the "api" middleware group. Make something great!
|
*/
// Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
// return $request->user();
// });
Route::get('/', function () {
return response()->json(['message' => 'hello world']);
});
同じくlocalhost
にアクセスして、jsonが返却されていればOKです。
1-3. Terraform環境構築
devcontainerの構築時、app/DockerfileでTerraformをすでにインストールしています。
そのため、改めてTerraformをインストールする必要はありません。
$ terraform -version
Terraform v1.5.0
on linux_arm64
Your version of Terraform is out of date! The latest version
is 1.12.1. You can update by downloading from https://www.terraform.io/downloads.html
ここでは、これからTerraformを利用するにあたって最低限必要なことだけ説明します。
version1.5.0
今回Terraformのversionは1.5.0に固定しています。
Terraformは記法や利用できる機能がversionに依存します。
versionを変更することで、すでに作成ずみのTerraformコードを書き換える必要が出てくる可能性があります。
もし1つのプロジェクトないし複数のプロジェクトにおいてTerraformを利用する場合は、各プロジェクトまたは依存するアプリのインフラごとにversionを可変することが求められます。tfenv
を利用することでバージョンを容易に変更できるので、必要であればそれをインストールしてversionを変更できるようにしておきます。
例)tfenvの利用方法
# tfenvインストール
$ brew install tfenv
# 利用可能なバージョン確認
$ tfenv list-remote
# terraformのバージョン指定インストール
$ tfenv install v1.5.0
# 現在使用中のversionとインストール済みのバージョン確認
# 使用中のものには*がつく
$ tfenv list
# version指定
$ tfenv use v1.5.0
aws cliの設定
こちらもdevcontainerの構築時、appのDockerfileですでにインストールしています。
インストールされているかどうかは、以下のコマンドで確認できます。
versionが表示されていれば、インストールできています。
$ aws --version
aws-cli/2.27.28 Python/3.13.3 Linux/5.15.49-linuxkit exe/aarch64.debian.11
インストールしたい場合は、以下のコマンドを実行します。
$ brew install awscli
プロファイルを設定
AWS CLIで、あるIAMユーザーの権限で操作するには、
- IAMユーザーのアクセスキーID
- シークレットアクセスキー
が必要なため、これを発行します。
もしすでにCLIを利用した経験があれば、以下のコマンドで現在使用中のプロファイルが確認できます。
$ aws configure list
利用した経験のない人は、以下の通り読み進めてください。
IAM選択
ブラウザから AWSマネジメントコンソール > IAM
に移動します。
AdministratorAccess
を持つIAMユーザーを選択(なければ作成)し、ユーザー名をクリック、その後認証情報
タブを選択肢 > アクセスキーの作成
をクリックします。すると、そのユーザーのIAMユーザーのアクセスキーID / シークレットアクセスキー
が確認できます。表示できれば、後述の設定で必要なので、画面を閉じずそのままにしておきます。
アクセスキーIDとシークレットアクセスキーは他人に知られないように
このキーを用いることでAWSリソースを簡単に操作できてしまいます。
もし他人に知られてしまうと、
- 高額な課金をされてしまう
- 意図しないリソースの変更がされてしまう
など悪用されてしまうリスクがあります。
うっかりGitHubのパブリックリポジトリにプッシュしてしまった、といったようなことがないようにしましょう。
AWS CLIでAWS操作
環境変数で設定する方法もありますが、プロファイルで設定した方が管理しやすいのでその方法を紹介します。
プロファイル設定
プロファイル作成は、コマンドで指示にしたがってキーの値を入力して設定します。
# プロファイル作成
# manager の部分は自身で識別しやすい任意のプロファイル名を指定
$ aws configure profile manager
# アクセスキーを入力
$ AWS Access Key ID [None]: [アクセスキーを指定]
# シークレットアクセスキーを入力
$ AWS Secret Access Key [None]: [シークレットアクセスキーを指定]
# リージョンを指定。以下は東京
$ Default region name [None]: ap-northeast-1
# CLI実行結果の出力形式
$ Default output format [None]: json
profile名を指定することで、対象の認証情報の一覧を表示できます。
$ aws configure list --profile [プロファイル名]
あるいは、デフォルトで指定したいprofile名があれば、以下を設定することでコマンドにprofileを設定せずに済みます。
# デフォルトのプロファイルを設定 例)manager
$ export AWS_PROFILE=manager
$ aws configure list
Name Value Type Location
---- ----- ---- --------
profile manager manual --profile
access_key ****************hoge shared-credentials-file
secret_key ****************fuga shared-credentials-file
region apnortheast-1 configfile ~/.aws/config
profileと書かれた行のValue値が、設定したプロファイル名であればOKです。
Terraformを使う中で、Authentication Error などに遭遇する場合は、aws configure list
コマンドで正しいプロファイル設定になっているかどうかを確認してください。
これまで設定したプロファイルの一覧は以下のコマンドで確認できます。
$ aws configure list-profiles
MFAを使って認証をよりセキュアに
多要素認証を用いることで、AWS認証をよりセキュアにすることができます。
共有されたアクセス情報の他にMFA認証を通すことで、
- 正規の操作ユーザーとして認可
- アクセストークンの有効期限
などセキュリティの高い開発ができます。
AWS CLI 動作確認
最後にaws cliコマンドを叩いて疎通確認をします。
以下のように表示されればOKです。
# cliキャッシュを削除し、疎通確認する
$ rm -rf ~/.aws/cli/cache && aws sts get-caller-identity --profile [プロファイル名]
{
"UserId": "ABCDEFGHIJKLMNOPQRSTUVWXWZ",
"Account": "771987654321",
"Arn": "arn:aws:iam::771987654321:user/manager"
}
これ以降は、Terraformを使ってインフラを構築します。
プロファイルを指定してリソースを構築したい場合は、
$ AWS_PROFILE=manager terraform apply
とすることで実現できます。
詳細は第2回の記事で紹介しならが進めたいと思います。
次回
ここまでで1.環境構築のすべてのセクションが終了しました。
次は、TerraFormでインフラ構築.ver1 で実際にTerraformを使ってインフラを構築します。
他の記事