この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
LINE事業部の太田です。
App RunnerがVPCリソースへのアクセスが可能になったので、RDS + App RunnerをCDKで構築してみたいと思います。
今回作ったものは以下にあります。
ECRを作成
CDKでECRを作成します。
- app-runner-example-stack.ts
import * as cdk from '@aws-cdk/core'
import * as ecr from '@aws-cdk/aws-ecr'
export class AppRunnerExampleStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
// ECR
const repository = new ecr.Repository(this, 'AppRunnerExampleRepository')
}
}
上記をデプロイすると、ECRが作成されます。
Webサーバーを用意
簡単なWebサーバーをExpressを使って構築します。
- src/index.ts
import express from 'express'
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.get('/', (req: express.Request, res: express.Response) => {
res.send('Hello World!')
})
app.listen(3000, () => {
console.log('Example app listening on port 3000!')
})
- package.json
{
"name": "app-runner-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "ts-node src/index.ts"
},
"dependencies": {
"express": "4.17.2"
},
"devDependencies": {
"@tsconfig/node14": "1.0.1",
"@types/express": "4.17.13",
"@types/node": "17.0.17",
"ts-node": "10.5.0",
"typescript": "4.5.5"
}
}
- Dockerfile
FROM node:14-slim
WORKDIR /usr/src/app
COPY . .
RUN yarn install --pure-lockfile --non-interactive
EXPOSE 3000
CMD ["yarn", "start"]
以下のコマンドでビルドしてECRにプッシュします。
$ aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
$ docker build -t app-runner-example .
$ docker tag app-runner-example ${REPOSITORY_URI}
$ docker push ${REPOSITORY_URI}
App Runnerを作成
CDKでApp Runnerを作成します。
- app-runner-example-stack.ts
import { AppRunner } from './app-runner'
export class AppRunnerExampleStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
// ECR
const repository = new ecr.Repository(this, 'AppRunnerExampleRepository')
// AppRunner
new AppRunner(this, 'AppRunner', { repository }) }
}
- app-runner.ts
import * as cdk from '@aws-cdk/core'
import * as iam from '@aws-cdk/aws-iam'
import * as apprunner from '@aws-cdk/aws-apprunner'
import * as ecr from '@aws-cdk/aws-ecr'
interface AppRunnerProps {
repository: ecr.Repository
}
export class AppRunner extends cdk.Construct {
constructor(scope: cdk.Construct, id: string, props: AppRunnerProps) {
super(scope, id)
const { repository } = props
// Roles
const instanceRole = new iam.Role(scope, 'AppRunnerInstanceRole', {
assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'),
})
const accessRole = new iam.Role(scope, 'AppRunnerAccessRole', {
assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'),
})
// Apprunner
new apprunner.Service(scope, 'AppRunnerExampleService', {
source: apprunner.Source.fromEcr({
imageConfiguration: {
port: 3000, // Webサーバーを3000ポートとしているので、ここを合わせます
},
repository,
tag: 'latest',
}),
instanceRole: instanceRole,
accessRole: accessRole,
})
}
}
上記をデプロイすると、App Runnerが作成されます。
デフォルトドメインにアクセスするとレスポンスが返ってきます。
$ curl https://xxxxxxxxxx.ap-northeast-1.awsapprunner.com
Hello World!
RDS・VPCを作成
App RunnerからRDSにアクセスできるようにしていきます。
RDSやVPCなどをCDKで作成します。
- app-runner-example-stack.ts
import * as cdk from '@aws-cdk/core'
import * as ecr from '@aws-cdk/aws-ecr'
import { AppRunner } from './app-runner'
import { Network } from './network'
import { Rds } from './rds'
export class AppRunnerExampleStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
super(scope, id, props)
// ECR
const repository = new ecr.Repository(this, 'AppRunnerExampleRepository')
// VPC
const { vpc, dbSecurityGroup } = new Network(this, 'Network')
// RDS
new Rds(this, 'Rds', { vpc, dbSecurityGroup })
// AppRunner
new AppRunner(this, 'AppRunner', { repository })
}
}
- network.ts
import * as cdk from '@aws-cdk/core'
import * as ec2 from '@aws-cdk/aws-ec2'
export class Network extends cdk.Construct {
readonly vpc: ec2.Vpc
readonly dbSecurityGroup: ec2.SecurityGroup
constructor(scope: cdk.Construct, id: string) {
super(scope, id)
this.vpc = new ec2.Vpc(this, 'VPC', {
cidr: '10.0.0.0/16',
subnetConfiguration: [
{
cidrMask: 24,
name: 'DB',
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
},
],
natGateways: 0,
})
// App Runnerに設定するセキュリティグループ
const AppRunnerSecurityGroup = new ec2.SecurityGroup(
this,
'AppRunnerSecurityGroup',
{
securityGroupName: 'app-runner-example-sg-ar',
vpc: this.vpc,
}
)
// RDSに設定するセキュリティグループ
this.dbSecurityGroup = new ec2.SecurityGroup(this, 'DBSecurityGroup', {
allowAllOutbound: true,
securityGroupName: 'app-runner-example-sg-db',
vpc: this.vpc,
})
// AppRunnerSecurityGroupからのポート5432のインバウンドを許可
this.dbSecurityGroup.addIngressRule(
AppRunnerSecurityGroup,
ec2.Port.tcp(5432)
)
}
}
- rds.ts
import * as cdk from '@aws-cdk/core'
import * as rds from '@aws-cdk/aws-rds'
import * as ec2 from '@aws-cdk/aws-ec2'
interface RdsProps {
vpc: ec2.Vpc
dbSecurityGroup: ec2.SecurityGroup
}
export class Rds extends cdk.Construct {
constructor(scope: cdk.Construct, id: string, props: RdsProps) {
super(scope, id)
const { vpc, dbSecurityGroup } = props
// RDSのパスワードを自動生成してSecret Managerに格納
const rdsCredentials = rds.Credentials.fromGeneratedSecret(
'appRunnerExample',
{ secretName: 'AppRunnerExampleDbSecret' }
)
new rds.DatabaseCluster(
scope,
'AppRunnerExampleDbCluster',
{
engine: rds.DatabaseClusterEngine.auroraPostgres({
version: rds.AuroraPostgresEngineVersion.VER_12_4,
}),
credentials: rdsCredentials,
instances: 1,
instanceProps: {
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.BURSTABLE3,
ec2.InstanceSize.MEDIUM
),
vpc,
vpcSubnets: vpc.selectSubnets({ subnetGroupName: 'DB' }),
securityGroups: [dbSecurityGroup],
},
defaultDatabaseName: 'appRunnerExample',
}
)
}
}
- app-runner.ts
import * as cdk from '@aws-cdk/core'
import * as iam from '@aws-cdk/aws-iam'
import * as apprunner from '@aws-cdk/aws-apprunner'
import * as ecr from '@aws-cdk/aws-ecr'
import * as secretsmanager from '@aws-cdk/aws-secretsmanager'
interface AppRunnerProps {
repository: ecr.Repository
}
export class AppRunner extends cdk.Construct {
constructor(scope: cdk.Construct, id: string, props: AppRunnerProps) {
super(scope, id)
...
// rds.tsで作成したRDSの接続情報をSecret Managerから取得
const secret = secretsmanager.Secret.fromSecretNameV2(
scope,
'AppRunnerExampleDbSecret',
'AppRunnerExampleDbSecret'
)
// Apprunner
new apprunner.Service(scope, 'AppRunnerExampleService', {
source: apprunner.Source.fromEcr({
imageConfiguration: {
port: 3000,
environment: {
// DBの接続情報を環境変数へ格納
dbUserName: secret.secretValueFromJson('username').toString(),
dbPassword: secret.secretValueFromJson('password').toString(),
dbHost: secret.secretValueFromJson('host').toString(),
dbPort: secret.secretValueFromJson('port').toString(),
dbName: secret.secretValueFromJson('dbname').toString(),
},
},
repository,
tag: 'latest',
}),
instanceRole: instanceRole,
accessRole: accessRole,
})
}
}
ここまでをデプロイすると、RDSやVCPが作成されます。
App RunnerからRDSに接続
App RunnerからRDSに接続するために設定をします。
CDKでできれば良いのですが、この辺りはまだCDKでは提供されてないようです。
App Runnerのコンソールに移動し、[設定]タブの[サービスを設定]の編集ボタンを押します。
編集画面になるので、ネットワークキングの設定で[カスタムVPC] - [新規追加]を押して以下のように設定します。
あとは、[追加] - [変更を保存]を押して終了です。
WebサーバーでRDSに接続
Webサーバー側でRDSに接続するように修正して、ECRにプッシュします。
ORMとしてprismaを利用します。
- src/index.ts
import express from 'express'
import * as prisma from '@prisma/client'
const app = express()
const prismaClient = new prisma.PrismaClient()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.get('/users/:id', async (req: express.Request, res: express.Response) => {
const id = req.params.id
const user = await prismaClient.user.findUnique({ where: { id: Number(id) } })
res.send(user ?? `User not found`)
})
app.post('/users', async (req: express.Request, res: express.Response) => {
const name = req.body.name
const user = await prismaClient.user.create({
data: {
name,
},
})
res.send(user)
})
app.listen(3000, () => {
console.log('Example app listening on port 3000!')
})
- package.json
{
"name": "app-runner-example",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "ts-node src/index.ts",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate deploy"
},
"dependencies": {
"@prisma/client": "3.9.2",
"express": "4.17.2"
},
"devDependencies": {
"@tsconfig/node14": "1.0.1",
"@types/express": "4.17.13",
"@types/node": "17.0.17",
"prisma": "3.9.2",
"ts-node": "10.5.0",
"typescript": "4.5.5"
}
}
- Dockerfile
FROM node:14-slim
WORKDIR /usr/src/app
# https://github.com/prisma/prisma/issues/1301#issuecomment-574317426
RUN apt-get -qy update
RUN apt-get -qy install openssl
COPY . .
RUN yarn install --pure-lockfile --non-interactive
RUN yarn db:generate
EXPOSE 3000
CMD sh -c "yarn db:migrate && yarn start"
- prisma/schema.prisma
prismaのDBスキーム定義です。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
あとは再度、ビルド&プッシュしてApp Runnerを再度デプロイします。
$ aws ecr get-login-password --region ${AWS_REGION} | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com
$ docker build -t app-runner-example .
$ docker tag app-runner-example ${REPOSITORY_URI}
$ docker push ${REPOSITORY_URI}
$ aws apprunner start-deployment --service-arn ${APP_RUNNER_ARN}
App Runnerのデプロイが完了すれば、RDSにアクセス可能になります。
$ curl -X POST 'https://xxxxxxxxxx.ap-northeast-1.awsapprunner.com/users' -d 'name=test'
{
"id":1,
"name":"test",
"createdAt":"2022-02-15T07:31:23.638Z",
"updatedAt":"2022-02-15T07:31:23.639Z"
}
$ curl https://xxxxxxxxxx.ap-northeast-1.awsapprunner.com/users/1
{
"id":1,
"name":"test",
"createdAt":"2022-02-15T07:31:23.638Z",
"updatedAt":"2022-02-15T07:31:23.639Z"
}
感想
実際に利用するにあたり以下の内容が個人的には気になりました。
自動デプロイ
App Runnerでは指定したタグ(上記の例ではlatest)を持つイメージがプッシュされると自動的にデプロイされる機能がありますが、タグは完全一致みたいなのでコミットのハッシュなどからタグを生成した場合は利用はできないかと思います。
その場合は手動デプロイで対応することになると思うので、タグのプレフィックスなどで自動デプロイが出来ると更に便利なのかなと思いました。
また、現在だと自動デプロイの設定はCDK側からは出来ないのでコンソール側から設定する必要がありそうです。
WAFは未対応
現在だとWAFは未対応の状態です。
要望としてWAF対応のissueがあがっているので今後対応されるかもしれません。
VPCの接続設定が手動
CDKではまだVPCの接続設定ができないので、手動で設定を入れる必要があります。
また、CDKのデプロイでApp Runnerが更新されると設定が解除されてしまうのでそのあたりも注意が必要かもしれません。
App Runnerでイメージのタグを変えたい場合は、AWS CLIで更新する必要があります。
Secret Managerから環境変数展開
Secret Mangerからの環境変数はCDK側で展開して環境変数にいれているので、コンソールからRDSのパスワードが見れてしまいます。
ECSのように直接展開できると便利かと思いました。
最後に
ECSやfargateを利用するよりは遥かに簡単に環境が構築できました。
今回は無理にCDKを使った感があるので、現在App Runnerを利用するならコンソールもしくはAWS CLIからの構築でも良いかと思います。
今後も機能拡張されてさらに便利になると思うので、利用していきたいです。