SvelteKit を AWS Fargate にデプロイしてみるまでの軌跡
とりあえずやってみる
当方の状況
インフラ経験が多い SRE
Svelte は一度も触ったことない
AWS は Solution Architect Pro は取れるくらいの知識
Terraform はほぼ毎日何かしら触っている状態
作業リポジトリ
ただのお勉強なので、Svelte アプリケーションと Terraform は同じリポジトリで管理していく
Hello World 的なものを AWS Fargate 上で動かせれば良い
一旦こんな感じの構造でやっていく
svelte-app ディレクトリは npm create svelte
の際に作成する
$ tree
.
├── svelte-app
└── terraform
Node.js は LTS のバージョンとした
$ node --version
v18.17.0
$ npm -version
9.6.7
https://svelte.jp/ を参考に npm create svelte@latest my-app
を実行
選択肢は TypeScript の部分だけ変更
~/W/g/practice-svelte-fargate ❯❯❯ npm create svelte@latest svelte-app
create-svelte version 5.0.3
┌ Welcome to SvelteKit!
│
◇ Which Svelte app template?
│ SvelteKit demo app
│
◇ Add type checking with TypeScript?
│ Yes, using TypeScript syntax
│
◇ Select additional options (use arrow keys/space bar)
│ none
│
└ Your project is ready!
✔ Typescript
Inside Svelte components, use <script lang="ts">
Install community-maintained integrations:
https://github.com/svelte-add/svelte-add
Next steps:
1: cd svelte-app
2: npm install (or pnpm install, etc)
3: git init && git add -A && git commit -m "Initial commit" (optional)
4: npm run dev -- --open
To close the dev server, hit Ctrl-C
Stuck? Visit us at https://svelte.dev/chat
ディレクトリ状況
~/W/g/practice-svelte-fargate ❯❯❯ tree
.
├── svelte-app
│ ├── README.md
│ ├── package.json
│ ├── src
│ │ ├── app.d.ts
│ │ ├── app.html
│ │ ├── lib
│ │ │ └── images
│ │ │ ├── github.svg
│ │ │ ├── svelte-logo.svg
│ │ │ ├── svelte-welcome.png
│ │ │ └── svelte-welcome.webp
│ │ └── routes
│ │ ├── +layout.svelte
│ │ ├── +page.svelte
│ │ ├── +page.ts
│ │ ├── Counter.svelte
│ │ ├── Header.svelte
│ │ ├── about
│ │ │ ├── +page.svelte
│ │ │ └── +page.ts
│ │ ├── styles.css
│ │ └── sverdle
│ │ ├── +page.server.ts
│ │ ├── +page.svelte
│ │ ├── game.ts
│ │ ├── how-to-play
│ │ │ ├── +page.svelte
│ │ │ └── +page.ts
│ │ ├── reduced-motion.ts
│ │ └── words.server.ts
│ ├── static
│ │ ├── favicon.png
│ │ └── robots.txt
│ ├── svelte.config.js
│ ├── tsconfig.json
│ └── vite.config.ts
└── terraform
11 directories, 28 files
npm create svelte
実行時に出力されたコマンドを実行
npm run dev -- --open
を実行すると添付の画面が表示された
~/W/g/practice-svelte-fargate ❯❯❯ cd svelte-app
~/W/g/p/svelte-app ❯❯❯ npm install
added 114 packages, and audited 115 packages in 14s
11 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
~/W/g/p/svelte-app ❯❯❯ npm run dev -- --open
> svelte-app@0.0.1 dev
> vite dev --open
Forced re-optimization of dependencies
VITE v4.4.7 ready in 931 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h to show help
ポートは 5173 で起動
Vite というものでデフォルトのポートが dev (port 5173) と preview (port 4173) を使ってるみたい
Node のコンテナで動かすため、https://kit.svelte.jp/docs/adapter-node を参考に adapter-node に変更
~/W/g/p/svelte-app ❯❯❯ npm i -D @sveltejs/adapter-node
added 23 packages, and audited 138 packages in 6s
17 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
~/W/g/p/svelte-app ❯❯❯
diff --git a/svelte-app/svelte.config.js b/svelte-app/svelte.config.js
index 1cf26a0..2214c60 100644
--- a/svelte-app/svelte.config.js
+++ b/svelte-app/svelte.config.js
@@ -1,4 +1,4 @@
-import adapter from '@sveltejs/adapter-auto';
+import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/kit/vite';
/** @type {import('@sveltejs/kit').Config} */
最終的には node build
コマンドでアプリケーションを実行
デフォルトポートは 3000
Dockerfile 関連を作成
リポジトリ直下に compose.yml を配置
docker/sveltekit に Dockerfile を配置
.
├── README.md
├── compose.yml
├── docker
│ └── sveltekit
│ └── Dockerfile
~~~ snip ~~~
├── static
│ ├── favicon.png
│ └── robots.txt
├── svelte.config.js
├── tsconfig.json
└── vite.config.ts
SvelteKit 用の Dockerfile
検証なので image は雑に lts-slim を指定
# ビルド用
FROM node:lts-slim as build
WORKDIR /app
COPY package*.json ./
COPY tsconfig.json ./
RUN npm ci
COPY . .
RUN npm run build
# デプロイ用
FROM node:lts-slim
RUN useradd svelteuser
USER svelteuser
WORKDIR /app
COPY --from=build --chown=svelteuser:svelteuser /app/build ./build
COPY --from=build --chown=svelteuser:svelteuser /app/package.json .
COPY --from=build --chown=svelteuser:svelteuser /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "./build"]
compose.yml は開発用ではなくデプロイ用(build 確認用)なので、volumes のマウントとかはせず
services:
sveltekit:
build:
context: .
dockerfile: docker/sveltekit/Dockerfile
ports:
- "3000:3000"
参考にさせていただいたサイト
build
~/W/g/p/svelte-app ❯❯❯ docker compose build --no-cache
[+] Building 13.7s (17/17) FINISHED
=> [sveltekit internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [sveltekit internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 572B 0.0s
=> [sveltekit internal] load metadata for docker.io/library/node:lts-slim 1.0s
=> CACHED [sveltekit builder 1/7] FROM docker.io/library/node:lts-slim@sha256:bfa807593c4e904c9dbdeec45a266d38040804e498c714bddf59734a1ed34730 0.0s
=> [sveltekit internal] load build context 0.3s
=> => transferring context: 359.99kB 0.3s
=> CACHED [sveltekit builder 2/7] WORKDIR /app 0.0s
=> [sveltekit stage-1 2/6] RUN useradd svelteuser 0.2s
=> [sveltekit stage-1 3/6] WORKDIR /app 0.0s
=> [sveltekit builder 3/7] COPY package*.json ./ 0.0s
=> [sveltekit builder 4/7] COPY tsconfig.json ./ 0.0s
=> [sveltekit builder 5/7] RUN npm ci 4.0s
=> [sveltekit builder 6/7] COPY . . 0.4s
=> [sveltekit builder 7/7] RUN npm run build 6.4s
=> [sveltekit stage-1 4/6] COPY --from=builder --chown=svelteuser:svelteuser /app/build ./build 0.0s
=> [sveltekit stage-1 5/6] COPY --from=builder --chown=svelteuser:svelteuser /app/package.json . 0.0s
=> [sveltekit stage-1 6/6] COPY --from=builder --chown=svelteuser:svelteuser /app/node_modules ./node_modules 0.4s
=> [sveltekit] exporting to image 0.6s
=> => exporting layers 0.6s
=> => writing image sha256:c9fe4e02357866bc6f15ed48b1887d6507223a0c9324f45456da0863a8bc3dfc 0.0s
=> => naming to docker.io/library/svelte-app-sveltekit 0.0s
up
~/W/g/p/svelte-app ❯❯❯ docker compose up -d
[+] Running 1/1
✔ Container svelte-app-sveltekit-1 Started 10.6s
動作確認
~/W/g/p/svelte-app ❯❯❯ curl localhost:3000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.png" />
<meta name="viewport" content="width=device-width" />
<link href="./_app/immutable/assets/0.2f593b13.css" rel="stylesheet">
<link href="./_app/immutable/assets/2.57239003.css" rel="stylesheet">
<link rel="modulepreload" href="./_app/immutable/entry/start.c96f72e8.js">
<link rel="modulepreload" href="./_app/immutable/chunks/scheduler.cbf234a0.js">
<link rel="modulepreload" href="./_app/immutable/chunks/singletons.29bf5bd2.js">
<link rel="modulepreload" href="./_app/immutable/chunks/index.14349a18.js">
<link rel="modulepreload" href="./_app/immutable/chunks/parse.bee59afc.js">
<link rel="modulepreload" href="./_app/immutable/entry/app.b8674e5f.js">
<link rel="modulepreload" href="./_app/immutable/chunks/index.d47c8428.js">
<link rel="modulepreload" href="./_app/immutable/nodes/0.bc578e9a.js">
<link rel="modulepreload" href="./_app/immutable/chunks/stores.77eaf609.js">
<link rel="modulepreload" href="./_app/immutable/nodes/2.c721e451.js"><title>Home</title><!-- HEAD_svelte-t32ptj_START --><meta name="description" content="Svelte demo app"><!-- HEAD_svelte-t32ptj_END -->
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents"> <div class="app svelte-8o1gnw"><header class="svelte-1u9z1tp"><div class="corner svelte-1u9z1tp" data-svelte-h="svelte-1jb641n"><a href="https://kit.svelte.dev" class="svelte-1u9z1tp"><img src="/_app/immutable/assets/svelte-logo.87df40b8.svg" alt="SvelteKit" class="svelte-1u9z1tp"></a></div> <nav class="svelte-1u9z1tp"><svg viewBox="0 0 2 3" aria-hidden="true" class="svelte-1u9z1tp"><path d="M0,0 L1,2 C1.5,3 1.5,3 2,3 L2,0 Z" class="svelte-1u9z1tp"></path></svg> <ul class="svelte-1u9z1tp"><li aria-current="page" class="svelte-1u9z1tp"><a href="/" class="svelte-1u9z1tp" data-svelte-h="svelte-5a0zws">Home</a></li> <li class="svelte-1u9z1tp"><a href="/about" class="svelte-1u9z1tp" data-svelte-h="svelte-iphxk9">About</a></li> <li class="svelte-1u9z1tp"><a href="/sverdle" class="svelte-1u9z1tp" data-svelte-h="svelte-1mtf8rh">Sverdle</a></li></ul> <svg viewBox="0 0 2 3" aria-hidden="true" class="svelte-1u9z1tp"><path d="M0,0 L0,3 C0.5,3 0.5,3 1,2 L2,0 Z" class="svelte-1u9z1tp"></path></svg></nav> <div class="corner svelte-1u9z1tp" data-svelte-h="svelte-1gilmbv"><a href="https://github.com/sveltejs/kit" class="svelte-1u9z1tp"><img src="/_app/immutable/assets/github.1ea8d62e.svg" alt="GitHub" class="svelte-1u9z1tp"></a></div> </header> <main class="svelte-8o1gnw"> <section class="svelte-19xx0bt"><h1 class="svelte-19xx0bt" data-svelte-h="svelte-11s73ib"><span class="welcome svelte-19xx0bt"><picture><source srcset="/_app/immutable/assets/svelte-welcome.c18bcf5a.webp" type="image/webp"> <img src="/_app/immutable/assets/svelte-welcome.6c300099.png" alt="Welcome" class="svelte-19xx0bt"></picture></span>
to your new<br>SvelteKit app</h1> <h2 data-svelte-h="svelte-1e36z0s">try editing <strong>src/routes/+page.svelte</strong></h2> <div class="counter svelte-y96mxt"><button aria-label="Decrease the counter by one" class="svelte-y96mxt" data-svelte-h="svelte-97ppyc"><svg aria-hidden="true" viewBox="0 0 1 1" class="svelte-y96mxt"><path d="M0,0.5 L1,0.5" class="svelte-y96mxt"></path></svg></button> <div class="counter-viewport svelte-y96mxt"><div class="counter-digits svelte-y96mxt" style="transform: translate(0, 0%)"><strong class="hidden svelte-y96mxt" aria-hidden="true">1</strong> <strong class="svelte-y96mxt">0</strong></div></div> <button aria-label="Increase the counter by one" class="svelte-y96mxt" data-svelte-h="svelte-irev0c"><svg aria-hidden="true" viewBox="0 0 1 1" class="svelte-y96mxt"><path d="M0,0.5 L1,0.5 M0.5,0 L0.5,1" class="svelte-y96mxt"></path></svg></button> </div> </section></main> <footer class="svelte-8o1gnw" data-svelte-h="svelte-1dlfr5"><p>visit <a href="https://kit.svelte.dev" class="svelte-8o1gnw">kit.svelte.dev</a> to learn SvelteKit</p></footer> </div>
<script>
{
__sveltekit_1vzhlsj = {
base: new URL(".", location).pathname.slice(0, -1),
env: {}
};
const element = document.currentScript.parentElement;
const data = [null,null];
Promise.all([
import("./_app/immutable/entry/start.c96f72e8.js"),
import("./_app/immutable/entry/app.b8674e5f.js")
]).then(([kit, app]) => {
kit.start(app, element, {
node_ids: [0, 2],
data,
form: null,
error: null
});
});
}
</script>
</div>
</body>
</html>
node の上位に nginx の web サーバを追加してアクセスできるようにしたい
docker ディレクトリ内に nginx 用ファイル群を作成
.
├── README.md
├── compose.yml
├── docker
│ ├── nginx
│ │ ├── Dockerfile
│ │ ├── default.conf
│ │ └── nginx.conf
│ └── sveltekit
│ └── Dockerfile
~~~ snip ~~~
Dockerfile はシンプルに 2 つの Nginx 用設定ファイルを配置
# デプロイ用
FROM nginx:stable
COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./docker/nginx/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
docker/nginx/nginx.conf はコンテナ内にあったデフォルトのものを利用
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
default.conf 側で SvelteKit が稼働する node サーバへの proxy 設定
upstream sveltekit {
server sveltekit:3000;
keepalive 8;
}
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
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 $scheme;
proxy_set_header Host $http_host;
proxy_pass http://sveltekit;
proxy_redirect off;
error_page 502 = @static;
}
location @static {
try_files $uri /index.html =502;
}
}
参考
compose.yml に nginx の設定を追加
services:
sveltekit:
build:
context: .
dockerfile: docker/sveltekit/Dockerfile
ports:
- "3000:3000"
nginx:
build:
context: .
dockerfile: docker/nginx/Dockerfile
ports:
- "80:80"
docker compose up -d --build
~/W/g/p/svelte-app ❯❯❯ docker compose up -d --build
[+] Building 6.2s (25/25) FINISHED
=> [nginx internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 240B 0.0s
=> [nginx internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [nginx internal] load metadata for docker.io/library/nginx:stable 0.0s
=> [nginx 1/3] FROM docker.io/library/nginx:stable 0.0s
=> [nginx internal] load build context 0.3s
=> => transferring context: 296B 0.3s
=> [sveltekit internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [sveltekit internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 572B 0.0s
=> [sveltekit internal] load metadata for docker.io/library/node:lts-slim 2.3s
=> CACHED [nginx 2/3] COPY ./docker/nginx/nginx.conf /etc/nginx/nginx.conf 0.0s
=> CACHED [nginx 3/3] COPY ./docker/nginx/default.conf /etc/nginx/conf.d/default.conf 0.0s
=> [nginx] exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:b12c261f15362f289dc769e96b47a020495c5b60e038f3b53f093939e91dcc2c 0.0s
=> => naming to docker.io/library/svelte-app-nginx 0.0s
=> [sveltekit internal] load build context 1.3s
=> => transferring context: 78.02MB 1.3s
=> CACHED [sveltekit builder 1/7] FROM docker.io/library/node:lts-slim@sha256:a0cca98f2896135d4c0386922211c1f90f98f27a58b8f2c07850d0fbe1c2104e 0.0s
=> => resolve docker.io/library/node:lts-slim@sha256:a0cca98f2896135d4c0386922211c1f90f98f27a58b8f2c07850d0fbe1c2104e 0.0s
=> [sveltekit stage-1 2/6] RUN useradd svelteuser 0.5s
=> [sveltekit stage-1 3/6] WORKDIR /app 0.1s
=> CACHED [sveltekit builder 2/7] WORKDIR /app 0.0s
=> CACHED [sveltekit builder 3/7] COPY package*.json ./ 0.0s
=> CACHED [sveltekit builder 4/7] COPY tsconfig.json ./ 0.0s
=> CACHED [sveltekit builder 5/7] RUN npm ci 0.0s
=> CACHED [sveltekit builder 6/7] COPY . . 0.0s
=> CACHED [sveltekit builder 7/7] RUN npm run build 0.0s
=> [sveltekit stage-1 4/6] COPY --from=builder --chown=svelteuser:svelteuser /app/build ./build 0.2s
=> [sveltekit stage-1 5/6] COPY --from=builder --chown=svelteuser:svelteuser /app/package.json . 0.0s
=> [sveltekit stage-1 6/6] COPY --from=builder --chown=svelteuser:svelteuser /app/node_modules ./node_modules 1.7s
=> [sveltekit] exporting to image 0.6s
=> => exporting layers 0.6s
=> => writing image sha256:9560ebe00034e18eb22f9371909a1fb494e9a79fa495d4de9b6f0fb2cbef30f5 0.0s
=> => naming to docker.io/library/svelte-app-sveltekit 0.0s
[+] Running 3/3
✔ Network svelte-app_default Created 0.0s
✔ Container svelte-app-sveltekit-1 Started 0.3s
✔ Container svelte-app-nginx-1 Started 0.4s
~/W/g/p/svelte-app ❯❯❯
curl でアクセス確認
ちゃんと 80 番ポートでアクセスできた
~/W/g/p/svelte-app ❯❯❯ curl localhost
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="./favicon.png" />
<meta name="viewport" content="width=device-width" />
<link href="./_app/immutable/assets/0.2f593b13.css" rel="stylesheet">
<link href="./_app/immutable/assets/2.57239003.css" rel="stylesheet">
<link rel="modulepreload" href="./_app/immutable/entry/start.cba8f3aa.js">
~~~ snip ~~~
SSG とかやるなら、nginx 側に静的ファイルを配置したりすることになりそう
その場合、adapter-node と adapter-static の両方を使うことになりそう?
Nginx と SvelteKit をつかった最低限の構成がローカルで動いたので、Fargate 環境にのせる方に向かう
GitHub Actions を使った ECR への push までを次の目標とする
Terraform は Terraform Cloud を利用する
terraform {
required_version = "~> 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.9.0"
}
}
backend "remote" {
hostname = "app.terraform.io"
}
}
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
Terraform = "True"
Repo = "practice-svelte-fargate"
}
}
}
Terraform Cloud の organization や workspace の値は公開したくないため、別ファイルで値を定義して terraform init 時に、--backend-config
を指定して読み込ませる
organization = "xxx"
workspaces {
name = "xxx"
}
terraform init 実行
terraform init --backend-config=tfbackend.hcl
今の Terraform ディレクトリ構成
~/W/g/p/terraform ❯❯❯ tree
.
├── terraform.tf
├── terraform.tfvars
├── tfbackend.hcl
└── variables.tf
以下 2 つの ECR のリポジトリ作成
- sveltekit
- nginx
for_each でループ処理しているが、repository_name はリスト形式なので toset を利用
locals {
repository_name = [
"sveltekit",
"nginx"
]
}
resource "aws_ecr_repository" "main" {
for_each = toset(local.repository_name)
name = each.value
}
resource "aws_ecr_lifecycle_policy" "main" {
for_each = aws_ecr_repository.main
policy = jsonencode(
{
rules = [
{
action = {
type = "expire"
}
description = "最新の2つを残してイメージを削除する"
rulePriority = 1
selection = {
countNumber = 2
countType = "imageCountMoreThan"
tagStatus = "any"
}
},
]
}
)
repository = each.value.name
}
一旦初期の動作確認用に .github/workflows/github_actions.yml を作成
AWS への接続は事前に OIDC 連携の設定と IAM ロール作成済みの状態で動作確認
defaults の working-directory でディレクトリを svelte-app に変更しておく
name: 'Deploy'
on:
push:
branches:
- main
defaults:
run:
working-directory: svelte-app
permissions:
id-token: write
contents: read
jobs:
build:
name: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: ap-northeast-1
role-to-assume: ${{ vars.AWS_IAM_ROLE_ARN }}
- run: aws sts get-caller-identity
- run: pwd
Docker image を build して push する GitHub actions
matrix を使って複数コンテナの build / push を並列化
uses を多く使ってみた
name: 'Deploy'
on:
push:
branches:
- main
defaults:
run:
working-directory: svelte-app
permissions:
id-token: write
contents: read
jobs:
build:
name: build
runs-on: ubuntu-latest
strategy:
matrix:
target: [
"nginx",
"sveltekit"
]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: ap-northeast-1
role-to-assume: ${{ vars.AWS_IAM_ROLE_ARN }}
# ECR ログイン
- name: ECR login
uses: aws-actions/amazon-ecr-login@v1
id: login-ecr
# Docker image のビルドとプッシュ
# matrix を使って複数の docker image をビルド
# uses の場合、working-directory は使えないのでフルパス指定
- name: Build and push
uses: docker/build-push-action@v4
with:
context: ./svelte-app
file: ./svelte-app/docker/${{ matrix.target }}/Dockerfile
push: true
tags: ${{ steps.login-ecr.outputs.registry }}/${{ matrix.target }}:${{ github.sha }}
残作業
- Fargate でコンテナ間通信(サイドカーとの通信)を行う場合、localhost or 127.0.0.1 を定義する必要があるので、nginx の proxy_pass の設定を書き換える
- Fargate 関連のリソース作成は以下のようなリソースを作成する
- VPC
- Public Subnet(NAT GW が高いため Private Subnet は使わない)
- IGW
- ALB 関連
- ECS クラスタ
- 各種 IAM
- ECS サービスの作成やデプロイは ecspresso を使う
nginx のコンフィグは GitHub Actions の中で sed で置換
docker tag に latest を付与するように修正
name: 'Deploy'
on:
push:
branches:
- main
paths:
- "svelte-app/**"
- ".github/**"
defaults:
run:
working-directory: svelte-app
permissions:
id-token: write
contents: read
jobs:
build:
name: build
runs-on: ubuntu-latest
strategy:
matrix:
target: [
"nginx",
"sveltekit"
]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: ap-northeast-1
role-to-assume: ${{ vars.AWS_IAM_ROLE_ARN }}
# ECR ログイン
- name: ECR login
uses: aws-actions/amazon-ecr-login@v1
id: login-ecr
- name: Update nginx parameters
run:
sed -i -e "s/sveltekit:3000/localhost:3000/g" docker/nginx/default.conf
# Docker image のビルドとプッシュ
# matrix を使って複数の docker image をビルド
# uses の場合、working-directory は使えないのでフルパス指定
- name: Build and push
uses: docker/build-push-action@v4
with:
context: ./svelte-app
file: ./svelte-app/docker/${{ matrix.target }}/Dockerfile
push: true
tags: ${{ steps.login-ecr.outputs.registry }}/${{ matrix.target }}:${{ github.sha }}, ${{ steps.login-ecr.outputs.registry }}/${{ matrix.target }}:latest
AWS リソースを作っていく
VPC 関連
とりあえず Public subnet とかだけ作成
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
}
resource "aws_subnet" "public_1a" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1a"
cidr_block = "10.0.0.0/24"
}
resource "aws_subnet" "public_1c" {
vpc_id = aws_vpc.main.id
availability_zone = "ap-northeast-1c"
cidr_block = "10.0.1.0/24"
}
resource "aws_route_table_association" "public_1a" {
subnet_id = aws_subnet.public_1a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_1c" {
subnet_id = aws_subnet.public_1c.id
route_table_id = aws_route_table.public.id
}
ECS 関連
- IAM
- タスクロール
- タスク実行ロール
- CW logs
- Security Group
- ECS クラスタ
- ECS サービスは escpresso で作成する
resource "aws_iam_role" "ecs-task-execution-role" {
name = "ecs-task-execution-role"
assume_role_policy = jsonencode(
{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
)
}
data "aws_iam_policy" "AmazonECSTaskExecutionRolePolicy" {
arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
resource "aws_iam_role_policy_attachment" "ecs-task-execution-role" {
role = aws_iam_role.ecs-task-execution-role.name
policy_arn = data.aws_iam_policy.AmazonECSTaskExecutionRolePolicy.arn
}
resource "aws_iam_role" "ecs-task-role" {
name = "ecs-task-role"
assume_role_policy = jsonencode(
{
"Version": "2008-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
)
}
resource "aws_iam_policy" "ecs-task-role" {
name = "ecs-task-role-policy"
policy = jsonencode(
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
)
}
resource "aws_iam_role_policy_attachment" "ecs-task-role" {
role = aws_iam_role.ecs-task-role.name
policy_arn = aws_iam_policy.ecs-task-role.arn
}
resource "aws_security_group" "practice-svelte-fargate" {
name = "practice-svelte-fargate"
# セキュリティグループを配置するVPC
vpc_id = aws_vpc.main.id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group_rule" "practice-svelte-fargate" {
security_group_id = aws_security_group.practice-svelte-fargate.id
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_cloudwatch_log_group" "main" {
name = "practice-svelte-fargate"
retention_in_days = 7
}
resource "aws_ecs_cluster" "main" {
name = "practice-svelte-fargate"
}
ecspresso の設定
ディレクトリ構成
.
├── svelte-app
│ ├── README.md
│ ├── compose.yml
│ ├── deployment
│ │ └── ecspresso
│ │ └── practice-svelte-fargate
│ │ ├── config.yml
│ │ ├── ecs-service-def.json
│ │ └── ecs-task-def.json
│ ├── docker
│ │ ├── nginx
~~~ snip ~~~
config.yml
Terraform Cloud を remote state の置き場所としているので、url で Terraform Cloud に関するものに設定
GitHub Actions 内で各種環境変数を設定する
region: ap-northeast-1
cluster: "{{ must_env `CLUSTER_NAME` }}"
service: practice-svelte-fargate
service_definition: "./ecs-service-def.json"
task_definition: "./ecs-task-def.json"
timeout: 5m
plugins:
- name: tfstate
config:
url: remote://app.terraform.io/{{ must_env `TFC_ORGANIZATION` }}/{{ must_env `CLUSTER_NAME` }}
ecs-service-def.json
一旦 ALB とかなしの状態で作る
{
"deploymentConfiguration": {
"deploymentCircuitBreaker": {
"enable": false,
"rollback": false
},
"maximumPercent": 200,
"minimumHealthyPercent": 100
},
"desiredCount": 1,
"enableECSManagedTags": false,
"launchType": "FARGATE",
"networkConfiguration": {
"awsvpcConfiguration": {
"assignPublicIp": "ENABLED",
"securityGroups": [
"{{ tfstate `aws_security_group.practice-svelte-fargate.id` }}"
],
"subnets": [
"{{ tfstate `aws_subnet.public_1a.id` }}",
"{{ tfstate `aws_subnet.public_1c.id` }}"
]
}
},
"placementConstraints": [],
"placementStrategy": [],
"platformVersion": "LATEST",
"schedulingStrategy": "REPLICA",
"serviceRegistries": []
}
ecs-task-def.json
nginx と sveltekit コンテナを利用したタスク
コンテナは一旦 latest 固定だが、おいおい修正
{
"containerDefinitions": [
{
"name": "nginx",
"command": [],
"entryPoint": [],
"environment": [],
"essential": true,
"image": "{{ tfstate `aws_ecr_repository.main['nginx'].repository_url` }}:latest",
"links": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "practice-svelte-fargate",
"awslogs-region": "ap-northeast-1",
"awslogs-create-group": "true",
"awslogs-stream-prefix": "nginx"
}
},
"mountPoints": [],
"portMappings": [
{
"containerPort": 80,
"hostPort": 80,
"protocol": "tcp"
}
],
"volumesFrom": []
},
{
"name": "sveltekit",
"command": [],
"entryPoint": [],
"environment": [],
"essential": true,
"image": "{{ tfstate `aws_ecr_repository.main['sveltekit'].repository_url` }}:latest",
"links": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "practice-svelte-fargate",
"awslogs-region": "ap-northeast-1",
"awslogs-create-group": "true",
"awslogs-stream-prefix": "sveltekit"
}
},
"mountPoints": [],
"portMappings": [
{
"containerPort": 3000,
"hostPort": 3000,
"protocol": "tcp"
}
],
"volumesFrom": []
}
],
"cpu": "256",
"executionRoleArn": "{{ tfstate `aws_iam_role.ecs-task-execution-role.arn` }}",
"taskRoleArn": "{{ tfstate `aws_iam_role.ecs-task-role.arn` }}",
"family": "practice-svelte-fargate",
"memory": "512",
"networkMode": "awsvpc",
"placementConstraints": [],
"requiresCompatibilities": ["FARGATE"],
"volumes": []
}
GitHub Actions に deploy job を追加
ecspresso をインストールして ecspresso deploy を実行するというシンプルな構成
TFE_TOKEN は Terraform Cloud から tfstate で情報を引っ張ってくるために必要
各種環境変数は事前に GitHub に登録しておく
deploy:
name: deploy
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: ap-northeast-1
role-to-assume: ${{ vars.AWS_IAM_ROLE_ARN }}
- name: Setup ecspresso
uses: kayac/ecspresso@v2
- name: Deploy
env:
CLUSTER_NAME: "practice-svelte-fargate"
TFC_ORGANIZATION: ${{ vars.TFC_ORGANIZATION }}
TFC_WORKSPACE: ${{ vars.TFC_WORKSPACE }}
TFE_TOKEN: ${{ secrets.TFE_TOKEN }}
run: |
ecspresso deploy --config ./deployment/ecspresso/practice-svelte-fargate/config.yml
ここまでで一旦 Fargate 上にアプリケーションをデプロイすることができた
ECS task に Public IP が付与されるので、それにアクセスして動作確認可能
~/W/g/practice-svelte-fargate ❯❯❯ curl -v 52.195.xxx.xxx
* Trying 52.195.xxx.xxx:80...
* Connected to 52.195.xxx.xxx (52.195.xxx.xxx) port 80 (#0)
> GET / HTTP/1.1
> Host: 52.195.xxx.xxx
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx/1.24.0
< Date: Tue, 01 Aug 2023 13:40:01 GMT
< Content-Type: text/html;charset=utf-8
< Content-Length: 4586
< Connection: keep-alive
< Vary: Accept-Encoding
< Last-Modified: Tue, 01 Aug 2023 13:12:06 GMT
< ETag: W/"4586-1690895526000"
~~~ snip ~~~
残作業
ALB を作ってその配下に ECS サービスを設定する
ACM もやっておく
ALB 作成
resource "aws_lb" "main" {
name = "practice-svelte-fargate"
load_balancer_type = "application"
security_groups = [
aws_security_group.practice-svelte-fargate.id
]
subnets = [
aws_subnet.public_1a.id,
aws_subnet.public_1c.id
]
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.main.arn
}
}
resource "aws_lb_target_group" "main" {
name = "practice-svelte-fargate-tg"
vpc_id = aws_vpc.main.id
target_type = "ip"
port = 80
protocol = "HTTP"
deregistration_delay = 60
health_check { path = "/" }
}
ecs-service-def.json に loadBalancers 項目追加
{
"deploymentConfiguration": {
"deploymentCircuitBreaker": {
"enable": false,
"rollback": false
},
"maximumPercent": 200,
"minimumHealthyPercent": 100
},
"desiredCount": 1,
"enableECSManagedTags": false,
"healthCheckGracePeriodSeconds": 0,
"launchType": "FARGATE",
"loadBalancers": [
{
"containerName": "nginx",
"containerPort": 80,
"targetGroupArn": "{{ tfstate `aws_lb_target_group.main.id` }}"
}
],
"networkConfiguration": {
"awsvpcConfiguration": {
"assignPublicIp": "ENABLED",
"securityGroups": [
"{{ tfstate `aws_security_group.practice-svelte-fargate.id` }}"
],
"subnets": [
"{{ tfstate `aws_subnet.public_1a.id` }}",
"{{ tfstate `aws_subnet.public_1c.id` }}"
]
}
},
"placementConstraints": [],
"placementStrategy": [],
"platformVersion": "LATEST",
"schedulingStrategy": "REPLICA",
"serviceRegistries": []
}
ALB のドメインでアクセスできるようになった
~ ❯❯❯ curl http://practice-svelte-fargate-xxxx.ap-northeast-1.elb.amazonaws.com/ -v
* Trying 35.76.xxx.xxx:80...
* Connected to practice-svelte-fargate-xxxx.ap-northeast-1.elb.amazonaws.com (35.76.xxx.xxx) port 80 (#0)
> GET / HTTP/1.1
> Host: practice-svelte-fargate-xxxx.ap-northeast-1.elb.amazonaws.com
> User-Agent: curl/7.88.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Date: Thu, 03 Aug 2023 13:51:21 GMT
< Content-Type: text/html;charset=utf-8
< Content-Length: 4585
< Connection: keep-alive
< Server: nginx/1.24.0
< Vary: Accept-Encoding
< Last-Modified: Thu, 03 Aug 2023 13:38:52 GMT
< ETag: W/"4585-1691069932000"
ACM は別に目的ではないし、一通り作業できたのでクローズ