Help us understand the problem. What is going on with this article?

ECS(Fargate)+APIGateway(Cognito認証)でAPIサーバーを構築する

前提条件/構成条件

  • APIサーバーはNginx + Golang
  • ユーザー情報はCognitoに格納されている
  • ユーザー認証をAPIGatewayのAuthorizerで行なう
  • CloudFront → API Gateway → Fargate

※AWSの設定作業はコンソールより手作業で行ないます
infra as codeはありません

構成図

0a352ed0c87e45014ce1-001.png

コンテナの準備

以下のDockerfileを準備します。
配置場所は当記事では{ルートdir}/release/app/および{ルートdir}/release/nginx/配下としています。

まずはGoで作成するアプリケーションです。

FROM golang:1.13.0-alpine as builder
ENV ROOT_PATH /go/src/github.com/xxx/yyy

WORKDIR $ROOT_PATH

RUN apk add --no-cache alpine-sdk git

RUN addgroup -g 10001 -S admin \
    && adduser -u 10001 -G admin -S admin

# modules
COPY go.mod go.sum ./
RUN go mod download

# application
COPY . .

# build
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/app && \
    chmod 755 /go/bin/app && \
    chown admin:admin /go/bin/app


FROM scratch

COPY --from=builder /etc/group /etc/group
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /go/bin/app /go/bin/app

USER admin

EXPOSE 9000
CMD ["/go/bin/app"]

アプリケーションはとりあえず以下な感じでよいでしょう。
go.modとgo.sumも配置しておいてください。

main.go
package main

import (
    "github.com/labstack/echo"
    "net/http"
)

func main() {
    e := echo.New()

    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello World!")
    })

    e.Logger.Fatal(e.Start(":9000"))
}

続いてNginxです。

FROM nginx:1.17.4-alpine
ADD ./release/nginx/nginx.conf /etc/nginx/conf.d/default.conf

Fargateは127.0.0.1で待ち受けます。
PortはGo側で設定したもの(9000)を設定します。

nginx.conf
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    include /etc/nginx/default.d/*.conf;

    location / {
        proxy_pass http://127.0.0.1:9000;
    }
}

AWS構築手順

VPC

VPCの作成

特に言及することはありません。
既に利用されているものがあればそのまま使いましょう

サブネットの作成

プライベートサブネットで作成します。
Fargateの場合はサーバーに固定IPを割り振りませんので、NATゲートウェイを設定しておきます。

当記事では以下の2つのCIDRブロックとしています。

  • 10.1.12.0/24(ap-northeast-1a)
  • 10.1.52.0/24(ap-northeast-1c)

後述するNATゲートウェイも作成する場合は、パブリックサブネットも一緒に作成しておきます。

スクリーンショット 2019-10-21 11.11.28.png

NATゲートウェイ

NATゲートウェイを作成します。ElasticIPが必要になりますので新しく作成しましょう。
 ※既に利用されているものがあればそのまま使いましょう

スクリーンショット 2019-10-21 11.14.58.png

作成後は、先ほど作成したサブネットのルートテーブルに紐付けるのを忘れないようにしましょう。

スクリーンショット 2019-10-21 11.15.56.png

※NATゲートウェイ自体が配置されているパブリックサブネットはインターネットゲートウェイが必要になります。
 忘れないように設定しておきましょう。

セキュリティグループ作成

続いてセキュリティグループを作成します。
このセキュリティグループはNLBからコンテナ(Nginx)へのアクセスで用いられます。
NLBについては後述します。以下のように先ほど作成したサブネットのCIDRブロックを記述して反映します。

スクリーンショット 2019-10-21 11.20.45.png

ロードバランサー

NLBの作成

APIGatewayからロードバランサーへルーティングする場合、VPCリンクで繋ぐ必要があります。
当記事ではAPIをVPCのプライベート空間(インターネットに公開しない)に配置するためNLBによる構築となります。

スキームは内部、リスナーはTLS(セキュアTCP)を選択します。
VPCは先ほど作成したVPCとサブネットを指定します。

スクリーンショット 2019-10-21 11.41.13.png

証明書

ACMに既に証明書が存在する場合はそのまま使いましょう。
当記事ではセキュリティポリシーをELBSecurityPolicy-TLS-1-2-2017-01を選択しています。

スクリーンショット 2019-10-21 11.45.20.png

ターゲットグループの作成

新しくターゲットグループを作成します。
ターゲットの種類はIPを選択します
次画面のターゲットの登録は、一旦スキップでOKです。

スクリーンショット 2019-10-21 11.47.48.png

ドメインの確保

NLBの作成が完了したらDNSを自身のドメインに設定します。
Route53を利用していればAliasレコードで登録できるかと思います。
当記事ではホスティングゾーンが別のためCNAMEで設定します。
 ※このドメインはサービスのエンドポイントではありません。
  CloudFront→①→APIGateway→②→NLB の②にあたる部分です。
  ①については後ほど設定します。

例: elb-origin.domain.com など

ECR

リポジトリの作成

ECRにリポジトリを作成しておきます。

スクリーンショット 2019-10-21 12.22.39.png

当記事ではリポジトリは以下の名称にしています。

  • {サービス名}/app
  • {サービス名}/nginx

これでコンテナのURIが以下のようになると思います。

  • {AWSのアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/{サービス名}/app
  • {AWSのアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/{サービス名}/nginx

コンテナのプッシュ

当記事の初めに準備したコンテナをプッシュします。
 ※CI環境では自動プッシュ/デプロイを設定することになります。今回は最初だけ手動プッシュとします。

まずはビルド

$ docker build --no-cache -t {appコンテナ名}:latest -f release/app/Dockerfile .
$ docker build --no-cache -t {nginxコンテナ名}:latest -f release/nginx/Dockerfile .

タグ付け

$ docker tag {appコンテナ名}:latest {ECRのコンテナURI}:latest
$ docker tag {nginxコンテナ名}:latest {ECRのコンテナURI}:latest

ECRにログインします
 ※awscliが使えない場合はインストールしてください

$ $(aws ecr get-login --no-include-email)

プッシュします

$ docker push {ECRのコンテナURI(app)}:latest
$ docker push {ECRのコンテナURI(nginx)}:latest

ECS

クラスターの作成

まずはクラスターを作成します。クラスターは単なる名前空間になります。
コンテナの設定、配置する数やスペックについては後述するサービスタスクによる定義で設定します。

クラスターテンプレートはネットワーキングのみを指定して作成します。

スクリーンショット 2019-10-21 12.09.59.png

IAMロールの作成

次にIAMロールを作成します。2つ作成します

タスクロール

このロールはコンテナ内部でAWSへのAPIリクエストを行なうためにタスクで使用するものです。
例えばS3にアクセスが必要なアプリケーションの場合、S3へのアクセスが可能なロールを定義します。

コンソールから作成する場合は、以下の項目を信頼されたエンティティとして作成します。
Elastic Container Service Task

スクリーンショット 2019-10-21 12.18.14.png

タスク実行ロール

このロールはタスクのプルやログの発行などを管理するロールです。
AmazonECSTaskExecutionRolePolicyというポリシーを付与して作成します。

スクリーンショット 2019-10-21 16.11.36.png

タスク定義の作成

コンソールから設定する場合、まずタスクロールタスク実行ロールを指定します。
先ほど作成したIAMロールを指定します。
タスク実行ロールにはAmazonECSTaskExecutionRolePolicyがアタッチされているロールを指定します。

スクリーンショット 2019-10-21 14.12.55.png

タスクサイズはとりあえず最低スペック(メモリ0.5GB、CPUが0.25vCPU)にしておきます。

続いてコンテナの追加ですが、ECRにプッシュしてあるappコンテナとnginxコンテナを指定します。
ヘルスチェックやログ出力などの細かい設定はとりあえずデフォルトのまま進めます。

nginxコンテナは、ポートマッピングを設定します。80を設定してください。

スクリーンショット 2019-10-21 14.19.20.png

appコンテナとnginxコンテナを設定できればタスク定義の作成を完了します。

サービスの作成

サービスはクラスターの中から作成することができます。

スクリーンショット 2019-10-21 14.33.09.png

ステップ1 サービスの設定

ステップ1のサービスの設定については特に難しくはありません。
起動タイプをFARGATEとし、タスク定義やクラスターは先ほど作成したものを選択します。
タスクの数は最低料金に抑えるため1にしておきます。(商用では適切なタスク数を検討してください)
デプロイメントについては、当記事ではローリングアップデートで進めます。

スクリーンショット 2019-10-21 14.38.28.png

ステップ2 ネットワーク構成

ステップ2のネットワーク構成です。
VPCとセキュリティグループは、先ほど作成したVPCとサブネット、セキュリティグループを選択します。
当記事では以下のサブネットを作成していました。

  • 10.1.12.0/24
  • 10.1.52.0/24

パブリックIPの自動割り当てについてはDISABLEDにしておきます。

続いて、ロードバランサーの設定です。先ほど作成したNLBを指定します。
ターゲットグループ名も、先ほど作成しているので選択します。

スクリーンショット 2019-10-21 14.44.22.png

サービスの検出(オプション)という項目については当記事ではOFFで設定します。

ステップ3 Auto Scaling(オプション)

ステップ3はAutoScaleingの設定です。
商用で利用する場合は必ず設定しましょう。

当記事では設定せずに先に進みます。

API Gateway

VPCリンクの作成

APIGatewayよりVPCリンクを作成します。
ターゲットNLBを先ほど作成したNLBとします。

スクリーンショット 2019-10-21 15.12.50.png

APIGatewayの作成

APIGatewayを作成します。

スクリーンショット 2019-10-21 15.36.57.png

メソッドの作成

一旦、/パスにGETメソッドを定義します。
統合タイプはVPCリンクを選択し、先ほど作成したVPCリンクを設定します。
エンドポイントURLは、先ほど作成したNLBのAliasレコード(もしくはCNAME)を設定します。
ここはNLBでインポートしたACMに対応するドメインである必要があります。

スクリーンショット 2019-10-21 16.03.17.png

APIのデプロイ

メソッドが作成したらデプロイを行ないます。(コンソールから作業するとよく忘れます。。)
ステージ名は適当にdevとでもしておきます。

CloudFront

Create Distribution

CloudFrontを設定していきます。
各項目については以下の通りです。これ以外の項目については要件により適宜設定してください。

項目 内容
Origin Domain Name 先ほど作成したAPIGatewayのドメイン 例: xxxxx.execute-api.ap-northeast-1.amazonaws.com (ステージ名は除く)
Origin Path APIGatewayのステージ名 例: /dev
Origin Protocol Policy HTTPS Only
Viewer Protocol Policy Redirect HTTP to HTTPS
Allowed HTTP Methods GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
Alternate Domain Names(CNAMEs) 自身のサービス用ドメイン(APIのエンドポイント)
SSL Certificate Custom SSL Certificate (example.com): (自身のACM証明書を選択)

ここまで出来れば、自身のドメインでアクセスすることでHello World!が表示されるはずです。

Authorizerの設定

Cognito

前提条件として、Cognitoにエンドユーザー(当APIにアクセスするユーザー)の情報が格納されていることとします。
Cognitoの扱い方については当記事では対象外となります。

APIGateway

新しいオーソライザーの作成

自身のAPIGatewayのメニューより、オーソライザー新しいオーソライザーの作成を選択します。
タイプをCognitoとし、ユーザープール名を指定します。
トークンのソースはAuthorizationとします。

スクリーンショット 2019-10-23 12.16.30.png

オーソライザーを作成したら、新しくPOSTのAPIおよびメソッドを作成します。
このAPIではCognitoより、認証されたユーザーの情報を受け取る検証をするためPOSTとしています。

オーソライザーは以下のメソッドリクエスト認可より設定します。

スクリーンショット 2019-10-23 12.19.32.png

次に統合リクエストマッピングテンプレートを選択します。
以下の形式でCognitoからのパラメータ群を取得することができます。

スクリーンショット 2019-10-23 12.28.34.png

{
  "sub": "$context.authorizer.claims.sub",
  "username": "$context.authorizer.claims['cognito:username']",
  "birthdate": "$context.authorizer.claims.birthdate",
  "gender": "$context.authorizer.claims.gender",
  ...(省略)
}

アプリケーション側(Go)では以下のように取得できるかと思います。

main.go
package main

import (
    "github.com/labstack/echo"
    "net/http"
)

type User struct {
    Sub string `json:"sub"`
    Username string `json:"username"`
    BirthDate string `json:"birthdate"`
    Gender string `json:"gender"`
    Name string `json:"name"`
    Locale string `json:"locale"`
    Email string `json:"email"`
    Picture string `json:"picture"`
}

func main() {
    e := echo.New()

    // ユーザーの取得
    e.POST("/test", func(c echo.Context) error {
        user := new(User)
        if err := c.Bind(user); err != nil {
            return err
        }
        return c.JSON(http.StatusOK, user)
    })

    e.Logger.Fatal(e.Start(":9000"))
}

CI構築

当記事ではCI環境をCodePipelineで行ないます。

CodeBuild

ビルドプロジェクトの作成

まずはCodeBuildを設定します。

送信元については、当記事ではGithubを設定しています。
ビルド環境についてはAmazon Linux 2を選択します。(好きなものでいいです)

スクリーンショット 2019-10-21 17.47.34.png

Buildspacについてはbuildspec.yamlを配置する予定のパスを指定します。

スクリーンショット 2019-10-21 17.55.23.png

buildspec.yamlの配置

内容は以下の通りです。

buildspec.yaml
version: 0.2
phases:
  install:
    runtime-versions:
      docker: 18
    commands:

  pre_build:
    commands:
      - $(aws ecr get-login --no-include-email)

  build:
    commands:
      - docker build --no-cache -t {appコンテナ名}:latest -f release/app/Dockerfile .
      - docker build --no-cache -t {nginxコンテナ名}:latest -f release/nginx/Dockerfile .
      - docker tag {appコンテナ名}:latest {appのコンテナURI}:latest
      - docker tag {nginxコンテナ名}:latest {nginxのコンテナURI}:latest

  post_build:
    commands:
      - docker push {appのコンテナURI}:latest
      - docker push {nginxのコンテナURI}:latest
      - |
        printf '[{"name":"app","imageUri":"%s"},{"name":"nginx","imageUri":"%s"}]' \
        {appのコンテナURI}:latest {nginxのコンテナURI}:latest > imagedefinitions.json

CodeDeploy

※CodeDeployについてはBlue/Greenデプロイを利用する場合のみ設定します。
 当記事ではローリングアップデートで進めるためこちらの作成は不要です。

CodePipeline

新規のパイプラインを作成する

続いてCodePipelineを作成します。
ソースステージについては、当記事ではGithubを選択しています。

スクリーンショット 2019-10-23 11.26.28.png

ビルドステージについては先ほど作成したプロジェクトを指定します。

スクリーンショット 2019-10-23 11.28.12.png

デプロイステージについてはAmazon ECSを指定します。
Blue/Greenデプロイの場合は別途Amazon ECS(ブルー/グリーン)を指定します。
イメージ定義ファイルはimagedefinitions.jsonです。CodeBuildのbuildspec.yamlで出力するJSONです。

スクリーンショット 2019-10-23 11.30.10.png

APIGatewayのデプロイ

APIGatewayについても、今後手動で設定していくのが辛いのでCI環境内でデプロイしたいものです。
今回は、CodeBuild上でデプロイします。
buildspec.yamlpost_build(ビルド完了後)に以下のコマンドを追記します
CodeBuiildの実行ロールにAPIGatewayの実行権限を付与するのを忘れないでください。

buildspec.yaml
# API Gateway Deploy
- aws apigateway put-rest-api --rest-api-id {APIGatewayのID} --mode overwrite --body file://openapi.yaml
- aws apigateway create-deployment --rest-api-id {APIGatewayのID} --stage-name {APIGatewayのステージ名}

ここでopenapi.yamlというファイルが登場しています。
こちらは、APIGatewayの構成を記述したOpenAPI(もしくはSwagger)形式のyamlとなります。
APIGatewayのステージエクスポートよりエクスポートができ、追記したものをインポートしデプロイすることができます。

アプリケーション側でAPI、メソッド等を追記したらこのファイルも更新しましょう。

openapi.yaml
openapi: "3.0.1"
info:
  title: "{Your API Title}"
  version: "xxx"
servers:
- url: "https://example.execute-api.ap-northeast-1.amazonaws.com/{basePath}"
  variables:
    basePath:
      default: "/dev"
paths:
  /:
    get:
      responses:
        200:
          description: "200 response"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Empty"
      x-amazon-apigateway-integration:
        uri: "https://elb-origin.domain.com/"
        responses:
          default:
            statusCode: "200"
        passthroughBehavior: "when_no_match"
        connectionType: "VPC_LINK"
        connectionId: "xxx"
        httpMethod: "GET"
        type: "http"
components:
  schemas:
    Empty:
      title: "Empty Schema"
      type: "object"

以上で全行程が完了です。

flatnyat
フリーランスエンジニア AWS Scala Go Rust PHP Node.js 専門はサーバーサイドです
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした