以前、CDKを使ってLambda関数をデプロイする記事を書きました。
同じCDKネタで、今度はDockerイメージをECSにデプロイすることを試してみましょう。
CDK
CDKコマンドのインストール
まず、CDKコマンドをインストールします。既にやっている場合は不要です。
CDKコマンドはNode.jsの16系か18系がサポートされているようなので、今回はNode.js 18.16.0を使用します。(バージョン切り替えにasdfを使っています。nodenvなどであればその書き方で。)
asdf local nodejs 18.16.0
npm i -g aws-cdk
CDKプロジェクトのセットアップ
適当なフォルダを作り、その中にまずcdkフォルダを作ります。使用する言語としてTypeScriptを指定してCDKプロジェクトを作成し、ひとまずsynth
します。
mkdir hello-ecs
cd hello-ecs
mkdir cdk
cd cdk
cdk init --language typescript
cdk synth
CDKのライブラリをインストールしておきます。
npm i --save aws-cdk-lib
CDKを使ってDockerイメージをECSにデプロイするにあたって、cdk-docker-image-deploymentというライブラリを使用します。このライブラリを使うと、ローカルのソースファイルからDockerイメージを作り、ECRにプッシュして、ECSにデプロイするという一連の流れを自動的にやってくれます。さらに、ALBを作ったロードバランサの設定なども行い(その辺のバリエーションはいろいろできそうです)、すぐに外部からアクセス出来る状態まで持って行ってくれます。
npm i --save cdk-docker-image-deployment
アプリ(Dockerイメージ)
Expressアプリの作成と動作確認
次に、この後デプロイすることになるDockerイメージのソースを作成します。cdkフォルダと並列にappフォルダを作成し、TypeScriptとExpressでアプリを開発するベースを作成します。
cd ..
mkdir app
cd app
npm init
npm i --save typescript @types/node ts-node nodemon
npx tsc --init
npm i --save express dotenv
npm i --save-dev @types/express
index.tsを作成し、下記のようにします。
CORSのあたりはガバガバですので、実際には上手く調整してください。
import 'dotenv/config'
import http from 'http'
import express from 'express'
const app: express.Express = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', '*')
res.header('Access-Control-Allow-Headers', '*')
next()
})
const server: http.Server = http.createServer(app)
server.listen(process.env.PORT, () => {
console.log(`Start on port ${process.env['PORT']}.`)
})
app.get('/', (req: express.Request, res: express.Response) => {
res.json({
result: 'OK'
})
})
使用するポートは.envで設定するようにしています。下記のような.envファイルを作成します。
PORT=3000
tsconfig.jsonに下記の設定を追加します。
"compilerOptions": {
"outDir": "./dist/",
}
package.jsonは下記のようにします。main
とscripts
を変更します。
"main": "dist/index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "nodemon --watch '**/*.ts' --exec 'ts-node' index.ts",
"build": "tsc",
"watch": "tsc -w",
"start": "yarn build && node dist/index.js"
},
この状態でアプリの動作をローカルで試せます。まず、Dockerを使わずに実行してみましょう。
npm run dev
curlでhttp://localhost:3000にアクセスすると、JSON文字列が表示されるでしょう。
動作確認できたら、Ctrl+Cで終了します。
Dockerイメージの設定とローカルでの動作確認
次に、Dockerで実行できるようにします。Dockerfileを作成し、下記のようにします。EXPOSEするポートは.envの設定内容を使用しますが、Dockerfileから直接.envファイルを参照できないので、docker-compose.ymlから渡された値を使うようにします。
FROM node:18.16.0
WORKDIR /usr/src/app
COPY package*.json .
RUN npm install
COPY . .
RUN npm run build
EXPOSE ${EXPOSE_PORT}
CMD [ "npm", "start" ]
docker-compose.ymlも作成します。
version: '3'
services:
app:
build:
context: .
dockerfile: Dockerfile
args:
- PORT=${PORT}
container_name: app
volumes:
- /usr/src/node_modules
- .:/usr/src/app
env_file: .env
ports:
- "${PORT}:${PORT}"
ここまで進んだら、ローカルのDockerで動作を確認してみましょう。
docker compose up -d
先ほどと同様にCurlでhttp://localhost:3000にアクセスすると、JSON文字列が表示されるでしょう。
動作確認できたら、docker compose down
で終了します。
再び、CDK
ここまでに作成したアプリをCDKでAWSにデプロイできるようにします。
CDKフォルダ内のlib/cdk-stack.ts
を下記のようにします。
デプロイする毎にECRのリポジトリにプッシュされますが、その際にバージョンタグを指定し、そのバージョンタグをECSへのデプロイ時に指定するようにしています。そのため、versionTag
の値はデプロイ毎に書き換える必要があります。
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2'
import * as ecs from 'aws-cdk-lib/aws-ecs'
import * as ecr from 'aws-cdk-lib/aws-ecr'
import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns'
import * as image_deploy from 'cdk-docker-image-deployment'
import * as path from 'path'
const versionTag: string = 'v0'
export class CdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const repository = new ecr.Repository(this, 'MyRepository', {
repositoryName: 'hello-ecs',
imageTagMutability: ecr.TagMutability.IMMUTABLE,
})
new image_deploy.DockerImageDeployment(this, 'imageDeployWithTag', {
source: image_deploy.Source.directory(path.join('..', 'app')),
destination: image_deploy.Destination.ecr(repository, {
tag: versionTag,
})
})
const vpc = new ec2.Vpc(this, 'MyVPC', {
maxAzs: 2, // ALBでは2以上
})
const cluster = new ecs.Cluster(this, 'MyCluster', {
vpc,
})
const taskDefinition = new ecs.FargateTaskDefinition(this, 'MyTaskDefinition', {
runtimePlatform: {
operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
cpuArchitecture: ecs.CpuArchitecture.X86_64,
}
})
taskDefinition.addContainer('MyAppContainer', {
image: ecs.ContainerImage.fromEcrRepository(repository, versionTag),
portMappings: [
{
containerPort: 3000,
}
]
})
const service = new ecs_patterns.ApplicationLoadBalancedFargateService(this, 'MyService', {
cluster,
taskDefinition,
})
}
}
まず、synthを行います。この時点でDockerイメージのビルドが行われます。
cdk synth
次にデプロイです。
AWSのアカウント、リージョン毎に一度だけcdk bootstrap
を実行する必要があります(プロジェクト毎に必要なわけではありません。)が、私の環境では冒頭に挙げた以前の記事の中でやっているので、省略します。
cdk deploy
デプロイ中にも再びDockerイメージのビルドが行われ、その結果がECRにプッシュされます。途中でIAMとセキュリティポリシの設定内容が表示され、確認が求められますので、y
と入力します。
デプロイが完了すると、ALBのLoadBalancerDNSと、実際にリクエスト可能なURLが表示されるので、実際にアクセスして動作を確認することができます。
クリーンアップ
cdk destroy
でクリーンアップできます。但し、cdk bootstrap
で実行された内容はクリーンアップされません。(同じアカウント、リージョンでデプロイする際に利用されます。)
cdk destroy