Closed37

SvelteKit を AWS Fargate にデプロイしてみるまでの軌跡

おくさんおくさん

とりあえずやってみる

当方の状況

インフラ経験が多い SRE
Svelte は一度も触ったことない
AWS は Solution Architect Pro は取れるくらいの知識
Terraform はほぼ毎日何かしら触っている状態

おくさんおくさん

一旦こんな感じの構造でやっていく
svelte-app ディレクトリは npm create svelte の際に作成する

$ tree
.
├── svelte-app
└── terraform
おくさんおくさん

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


おくさんおくさん

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} */

おくさんおくさん

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 を指定

docker/sveltekit/Dockerfile
# ビルド用
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 のマウントとかはせず

compose.yml
services:
  sveltekit:
    build:
      context: .
      dockerfile: docker/sveltekit/Dockerfile
    ports:
      - "3000:3000"

参考にさせていただいたサイト
https://qiita.com/mkin/items/5b4fb2e61cf47fc5f904
https://medium.com/@alpercitak/dockerize-sveltekit-1323771a12b1

おくさんおくさん

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>
おくさんおくさん

docker ディレクトリ内に nginx 用ファイル群を作成

.
├── README.md
├── compose.yml
├── docker
│   ├── nginx
│   │   ├── Dockerfile
│   │   ├── default.conf
│   │   └── nginx.conf
│   └── sveltekit
│       └── Dockerfile

~~~ snip ~~~

Dockerfile はシンプルに 2 つの Nginx 用設定ファイルを配置

docker/nginx/Dockerfile
# デプロイ用
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 はコンテナ内にあったデフォルトのものを利用

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 設定

default.conf
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;
    }
}

参考
https://stackoverflow.com/questions/70398935/how-to-deploy-a-svelte-kit-app-after-build-using-nginx-as-web-server

おくさんおくさん

compose.yml に nginx の設定を追加

compose.yml
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 ~~~
おくさんおくさん

Nginx と SvelteKit をつかった最低限の構成がローカルで動いたので、Fargate 環境にのせる方に向かう
GitHub Actions を使った ECR への push までを次の目標とする

おくさんおくさん

Terraform は Terraform Cloud を利用する

terraform.tf
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 を指定して読み込ませる

tfbackend.tf(Git で管理しない)
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 を利用

ecr.tf
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 に変更しておく

.github/workflows/github_actions.yml
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 を多く使ってみた

github_actions.yml
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 を付与するように修正

github_actions.yml
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
おくさんおくさん

VPC 関連
とりあえず Public subnet とかだけ作成

network.tf
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 で作成する
ecs.tf
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 内で各種環境変数を設定する

config.yml
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 とかなしの状態で作る

ecs-service-def.json
{
  "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 固定だが、おいおい修正

ecs-task-def.json
{
  "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 で情報を引っ張ってくるために必要
https://github.com/kayac/ecspresso#supported-tfstate-url-format

各種環境変数は事前に 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 項目追加

ecs-service-def.json
{
  "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"
このスクラップは2023/08/03にクローズされました