🏃

AWS CDKを使って、App Runnerでアプリを楽々デプロイする方法

2024/01/10に公開

はじめに

App Runnerを利用してサービスでも作ろうとした時に、コマンド一つでデプロイできないかなーと試してみた内容です。

読むと幸せになるかもしれない人

  • DockerイメージのビルドとECRへのプッシュをコマンドで実行している人
  • AWSのコンソール画面にて、App Runnerをデプロイしている人
  • App Runnerを取り敢えず使ってみたい人

コードについて

ディレクトリ構成

ディレクトリ構成は以下のようにしています。
今回のサンプルでは、Rustで作成したアプリをApp Runnerにて動作させるようにしています。

.
├── backend
│   ├── .gitignore
│   ├── Cargo.toml
│   ├── Dockerfile
│   └── src
│       └── main.rs
└── cdk
    ├── .gitignore
    ├── .npmignore
    ├── README.md
    ├── bin
    │   └── cdk.ts
    ├── cdk.json
    ├── jest.config.js
    ├── lib
    │   ├── cdk-stack.ts
    │   └── constructs
    │       ├── AppRunner.ts
    │       └── ImageRepository.ts
    ├── package-lock.json
    ├── package.json
    ├── test
    │   └── cdk.test.ts
    └── tsconfig.json

CDKコード

Dockerイメージのビルド、ECRへのプッシュ

ImageRepository.tsにて、cdk-docker-image-deploymentを利用しており、Dockerイメージのビルドと、ECRへのプッシュ処理を行うようにしています。
https://github.com/cdklabs/cdk-docker-image-deployment

ImageRepository.ts
import { IRepository, Repository, TagMutability } from "aws-cdk-lib/aws-ecr";
import {
	Destination,
	DockerImageDeployment,
	Source,
} from "cdk-docker-image-deployment";
import { Construct } from "constructs";
import path = require("path");

export class ImageRepository extends Construct {
	repository: IRepository;

	constructor(scope: Construct, id: string) {
		super(scope, id);

		const repository = new Repository(this, "RustBackendRepository", {
			repositoryName: "rust_backend",
			imageTagMutability: TagMutability.IMMUTABLE,
		});

		const result = new DockerImageDeployment(this, "RustBackendImage", {
			source: Source.directory(path.join('..', 'backend')),
			destination: Destination.ecr(repository, {
				tag: "v1",
			}),
		});

		this.repository = repository;
	}
}

App Runnerのデプロイ

ImageRepository.tsにて、作成したリポジトリ情報を元に、App Runnerへデプロイします。
App Runnerのライブラリについて、CDK v2のリファレンスが見つからなく、実装に苦労しました😰

AppRunner.ts
import { IRepository } from "aws-cdk-lib/aws-ecr";
import {
	aws_apprunner as apprunner,
} from 'aws-cdk-lib';
import { Construct } from "constructs";

export type AppRunnerProps = {
	repository: IRepository;
};

export class AppRunner extends Construct {
	constructor(scope: Construct, id: string, props: AppRunnerProps) {
		super(scope, id);

		const appRunnerECRAccessRole = process.env.ECR_ARN;
		const { repository } = props;

		const cfnService = new apprunner.CfnService(this, 'AppRunnerService', {
			sourceConfiguration: {
				authenticationConfiguration: {
					accessRoleArn: appRunnerECRAccessRole,
				},
				autoDeploymentsEnabled: false,
				imageRepository: {
					imageIdentifier: repository.repositoryUriForTag("v1"),
					imageRepositoryType: 'ECR',
					imageConfiguration: {
						port: '8080',
						runtimeEnvironmentVariables: [],
					},
				},
			},
			healthCheckConfiguration: {
				interval: 5,
				healthyThreshold: 2,
			},
		});
	}
}

CDK Stack

ECRへのプッシュ完了後に、App Runnerのデプロイ作業を行わないとエラーになってしまうため、addDependencyを利用して依存関係を明示しています。

cdk-stack.ts
import { Construct } from 'constructs';
import {
  Stack,
  StackProps,
} from 'aws-cdk-lib';
import { ImageRepository } from './constructs/ImageRepository';
import { AppRunner } from './constructs/AppRunner';

export class CdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const imageRepository = new ImageRepository(this, "ImageRepository");

    const backendApp = new AppRunner(this, "RustBackend", {
      repository: imageRepository.repository,
    });

    backendApp.node.addDependency(imageRepository);
  }
}

バックエンドコード

とりあえず、「Hello World!」と返す簡単なWeb APIサーバーです。

main.rs
use actix_web::{get, web, App, HttpServer, Responder};

#[get("/hello/{name}")]
async fn greet(name: web::Path<String>) -> impl Responder {
    format!("Hello {name}!")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(|| async { "Hello World!" }))
            .route("/hello", web::get().to(|| async { "Hello World!!" }))
            .service(greet)
    })
    .bind(("0.0.0.0", 8080))?
    .run()
    .await
}

Dockerファイルです。

Dockerfile
FROM rust:1.71 as builder
WORKDIR /usr/src/backend
COPY . .
RUN cargo install --path .

FROM centos:latest
COPY --from=builder /usr/local/cargo/bin/backend /usr/local/bin/backend
CMD ["backend"]

デプロイ確認

cdk deployコマンドを実行し、App Runnerへデプロイが完了すればOKです!

改善点

現在のコードだと、ECRのタグ指定方法をハードコーディングしてしまっているため、コマンドライン引数にて、タグの値を取得してきた方が良さそうです。
また、過去にデプロイしたDockerイメージへ巻き戻しする場合もあり得るので、Dockerイメージのビルド有無も指定できるようにもした方が良さそうです。

さいごに

これまで手間だと感じていたことが、コマンド一発でやれるようになるのは気持ちいいですね…☺️
これでRustを利用したサービスを作成する準備ができたので、次回はRustの記事にてお会いしましょう!

コラボスタイル Developers

Discussion