はじめに
AWS App Runnerとは
AWS App Runner は、コンテナ化されたウェブアプリケーションや API を開発者が簡単かつ迅速にデプロイできるフルマネージド型サービスです。大規模に、しかも事前のインフラ経験を必要とせずにデプロイすることができます。ソースコードからでも、コンテナイメージからでも始められます。App Runner は、ウェブアプリケーションを自動的に構築してデプロイし、暗号化を利用するトラフィックの負荷を分散し、トラフィックのニーズに合わせてスケールし、お客様のサービスがプライベート Amazon VPC で実行される他の AWS のサービスやアプリケーションと簡単に通信できるようにします。App Runner を使用すれば、サーバーやスケーリングについて煩わされることもなく、アプリケーションに集中できる時間が増えます。
(AWS App Runner)
特徴として下記が挙げられます。
- コンテナ動作環境のフルマネージドサービス
- Amazon ECRへのPushをトリガーにBlue/Green方式で安全にデプロイされる
- イメージのビルドなども含めたCI/CDを利用できる(一部言語のみ)
- VPCなどのインフラリソースを気にせずにコンテナをデプロイできる
VPCリソースとの接続について
AWS App Runnerは、VPCなどのインフラリソースを気にせず利用できる反面、リリース当時はプライベートなVPCリソース(プライベートサブネット内のAmazon RDSなど)と接続できないという制約がありました。そのため、Webアプリケーションをデプロイする際にはDynamoDBやパブリックアクセス可能なRDSのなどを利用する必要がありました。
その制約が2022/02/08のアップデートにて、「VPCコネクタ」というリソースを作成することでプライベートサブネットへアクセス可能になり、App Runner上で展開できるアプリケーションの幅が広がりました。
App Runner の新機能 — Amazon Virtual Private Cloud (VPC) をサポート
今回は実際にVPCコネクタを作成してプライベートサブネット内のAmazon RDSをApp Runnerから利用してみたいと思います。
サンプルアプリケーション
今回作成するのは、画像とテキストのセットでアップロードできる、簡易日記アプリのバックエンドAPIです。
システム構成図は以下のようになります。

VPC/RDSの準備
VPC・サブネットを作成します。
RDS用にプライベートサブネットを2AZに作成しておきます。

RDSはプライベートサブネットに配置し、外部から直接アクセスしないようにします。

セキュリティグループの作成
App RunnerからVPCにアクセスするために、「カスタムVPCコネクタ」というものを作成する必要があります。
その際、コネクタにSGをアタッチすることになるため、あらかじめ作成しておきます。
インバウンド/アウトバウンド のルール設定は特に必要ありません。

また、RDSのSGのインバウンドを修正し、カスタムVPCコネクタのSGからアクセスできるようにしておきます。

IAMロールを作成
App RunnerからS3にアクセスするため、必要なポリシーがアタッチされたロールを作成します。
AWSサービスのユースケース一覧には2022/05/27現在、App Runnerが表示されないため、カスタム信頼関係ポリシーから、下記のように設定してください。

1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "Version": "2012-10-17", "Statement": [ { "Sid": "", "Effect": "Allow", "Principal": { "Service": "tasks.apprunner.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } |
デプロイするアプリケーションを作成
今回のアプリケーションはGolangを利用し、echo + gormでサクッと作成しました。
▼ソースコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
package main import ( "os" "io" "log" "net/http" "github.com/labstack/echo/v4" "gorm.io/driver/postgres" "gorm.io/gorm" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/s3/s3manager" ) var DB *gorm.DB var err error type Diary struct { gorm.Model Text string Img string } func connect() (database *gorm.DB, err error) { USER := os.Getenv("DB_USER") PASS := os.Getenv("DB_PASSWORD") ENDPOINT := os.Getenv("DB_ENDPOINT") PORT := os.Getenv("DB_PORT") DBNAME := os.Getenv("DB_NAME") CONNECT := "host=" + ENDPOINT + " " + "port=" + PORT + " " + "user=" + USER + " " + "dbname=" + DBNAME + " " + "password=" + PASS return gorm.Open(postgres.Open(CONNECT), &gorm.Config{}) } func upload2S3(src io.Reader, fileName string) (string, error) { BUCKET := os.Getenv("BUCKET") sess, err := session.NewSessionWithOptions(session.Options{ Config: aws.Config{Region: aws.String("ap-northeast-1")}, }) uploader := s3manager.NewUploader(sess) _, err = uploader.Upload(&s3manager.UploadInput{ Bucket: aws.String(BUCKET), Key: aws.String(fileName), Body: src, }) if err != nil { log.Print(err) return "", err } return fileName, nil } func getDiaries(c echo.Context) error { diaries := []Diary{} DB.Find(&diaries) return c.JSON(http.StatusOK, diaries) } func postDiary(c echo.Context) error { imgPath := "" file, err := c.FormFile("file") if err == nil { src, err := file.Open() if err != nil { return err } defer src.Close() imgPath, err = upload2S3(src, file.Filename) if err != nil { return err } } record := Diary{Text: c.FormValue("text"), Img: imgPath} DB.Create(&record) return c.JSON(http.StatusOK, record) } func main() { DB, err = connect() if err != nil { log.Fatalln("database can't connect") } DB.AutoMigrate(&Diary{}) e := echo.New() e.GET("/", getDiaries) e.POST("/", postDiary) e.Logger.Fatal(e.Start(":8080")) } |
▼Dockerfile
1 2 3 4 5 6 7 8 9 10 11 |
FROM golang:1.18.0-bullseye as gobuilder WORKDIR /go/src/app COPY go.mod go.sum ./ RUN go mod download COPY main.go ./ RUN go build -ldflags="-w -s" -o /go/bin/app FROM gcr.io/distroless/base COPY --from=gobuilder /go/bin/app / EXPOSE 8080 CMD ["/app"] |
簡略化のため、DBへの接続情報は全て環境変数から取得していますが、本番利用する際はSystem Manager ParameterStoreやSecrets Managerなどから取得するようにしたほうがセキュリティの観点では良いでしょう。
App Runnerにアプリケーションをデプロイ
ECR リポジトリを準備
Amazon ECRにプライベートリポジトリを作成します。

「プッシュコマンドの表示」を開いておきます。

ECRへイメージをプッシュ
表示されているコマンドを上から順に実行し、ECRへイメージをプッシュします。


App Runner にサービスを作成
App Runnerにサービスを作成します。

今回は
- コンテナレジストリ
- Amazon ECR
を利用します。
先程準備したECRリポジトリを選択します。

スペックと環境変数を設定します。

「セキュリティ」を開き、あらかじめ作成しておいたインスタンスロールを選択します。

「ネットワーキング」を開き、「カスタムVPC」を選択します。
ここで、今回の目玉であるVPCコネクタを新規追加します。

対象VPC/サブネットを選択し、セキュリティグループはあらかじめ作成しておいたものを選択します。

VPCコネクタを追加できたことを確認し、「次へ」をクリックします。

構成を確認し、「作成とデプロイ」をクリックします。
作成には数分かかります。

動作確認
POST
エンドポイントにアクセスして、まずはテキストのみ送信してみましょう。
1 2 3 |
$ curl -X POST https://<endpoint>.ap-northeast-1.awsapprunner.com -F text="今日は朝からランニングをした" {"ID":1,"CreatedAt":"2022-05-27T00:41:38.224026611Z","UpdatedAt":"2022-05-27T00:41:38.224026611Z","DeletedAt":null,"Text":"今日は朝からランニングをした","Img":""} |
良さそうですね!
GET
データの取得をしてみます。
1 2 3 |
$ curl -X GET https://<endpoint>.ap-northeast-1.awsapprunner.com [{"ID":1,"CreatedAt":"2022-05-27T00:41:38.224026Z","UpdatedAt":"2022-05-27T00:41:38.224026Z","DeletedAt":null,"Text":"今日は朝からランニングをした","Img":""}] |
ちゃんとRDSへアクセスしてデータの読み書きができていますね!!
POST(画像あり)
次は画像をセットで送信してみましょう。
1 2 3 |
$ curl -X POST https://<endpoint>.ap-northeast-1.awsapprunner.com -F text="昨日の晩ごはん" -F file=@img/dinner.jpg upstream connect error or disconnect/reset before headers. reset reason: connection termination |
エラーが出てしまいました。
ログを確認すると、S3エンドポイントへのアップロードがタイムアウトしているようです。

注意点
VPC に接続すると、AppRunner サービスからすべてのアウトバウンドトラフィックが VPC ルーティングルールに基づいてルーティングされます。NAT ゲートウェイへのルートで許可されない限り、サービスはパブリックインターネット (AWS API を含む) にアクセスできません。また、Amazon Simple Storage Service (Amazon S3)や Amazon DynamoDB などの AWS API に接続するように VPC エンドポイントを設定して、NAT トラフィックを回避することもできます。
(App Runner の新機能 — Amazon Virtual Private Cloud (VPC) をサポート)
カスタムVPCコネクタを作成した場合、App Runnerアプリケーションからの全てのアウトバウンド通信が、VPCを経由して送信されます。
今回、S3への画像アップロードはアプリケーションから実施しているので、プライベートサブネットからS3にアクセスできなかったと考えられます。
S3へ接続するためにVPCエンドポイントを作成するため、構成図は下記のようになります。

VPCエンドポイントを作成
プライベートサブネットから利用できるVPCエンドポイントを作ります。

再度確認
1 2 3 |
$ curl -X POST https://<endpoint>.ap-northeast-1.awsapprunner.com -F text="昨日の晩ごはん" -F file=@img/dinner.jpg {"ID":2,"CreatedAt":"2022-05-27T05:10:29.584587842Z","UpdatedAt":"2022-05-27T05:10:29.584587842Z","DeletedAt":null,"Text":"昨日の晩ごはん","Img":"dinner.jpg"} |
今度はうまく動きました!

まとめ
今回はAWS App Runnerのアップデートで追加されたカスタムVPCコネクタを利用して、VPCリソースのアクセスを検証してみました。すべてのアウトバウンド通信がVPCへ流れるので、VPC外リソースへのアクセスが必要な場合は、VPCエンドポイント or NAT Gatewayが必要になりますので、実際に利用する際はご注意ください。
Amazon RDSと組み合わせて利用したいという要望は多かったと思うので、このアップデートでかなり利用しやすくなったかと思います。コンテナ環境が手軽に扱えるようになるのはとてもよい更新ですね!
個人的にはあと、AWS WAFとの連携ができるようになると一層使いやすくなるのではないかなと思っております。(apprunner-roadmapにも要望は上がっているので、いつか対応するかもしれません)
—
AWS App Runnerを含む、コンテナ基盤の導入を検討、お困りのお客様、是非スカイアーチネットワークスまでご相談ください!
投稿者プロフィール

- AWSのサーバレス構成を中心に構築業務に携わっております
最新の投稿
AWS2022年5月27日App Runner新機能でVPCリソースを利用してみた
AWS2021年9月27日AWS GameDay Online に参加しました! ~ APN杯 vol.2 ~