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

## 前提条件/構成条件
- APIサーバーはNginx + Golang
- ユーザー情報はCognitoに格納されている
- ユーザー認証をAPIGatewayのAuthorizerで行なう
- CloudFront → API Gateway → Fargate

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

## 構成図

![0a352ed0c87e45014ce1-001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/64de08b6-5ec2-5093-4275-1f644869fa92.png)


## コンテナの準備

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

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

```Dockerfile
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です。

```Dockerfile
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/bc96c3fd-43ea-b851-199c-9017b8fef075.png)

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

![スクリーンショット 2019-10-21 11.14.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/ec08e6b2-3718-2610-6d65-9f8d3d1185af.png)

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

![スクリーンショット 2019-10-21 11.15.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/92d97d41-fc7c-70af-1e8b-ffd94676007d.png)

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

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

![スクリーンショット 2019-10-21 11.20.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/15bfbdba-e835-9ce4-248f-161cbd5e51b0.png)

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

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

![スクリーンショット 2019-10-21 11.41.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/07051f81-f9cd-e2ef-94fb-d01e1ff98e81.png)

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

![スクリーンショット 2019-10-21 11.45.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/8237239b-a0af-1811-14e2-244fa0a87674.png)

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

![スクリーンショット 2019-10-21 11.47.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/db53b27e-64a3-b8b3-d212-3e0799c2e3f8.png)

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

例: elb-origin.domain.com など

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

![スクリーンショット 2019-10-21 12.22.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/ceea3be4-8037-deff-52a7-46e47ec96ced.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/68e640ae-1fb2-edf4-a160-19de2e9b6f4e.png)

#### IAMロールの作成
次にIAMロールを作成します。２つ作成します

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

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

![スクリーンショット 2019-10-21 12.18.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/17fa6c2e-5832-4895-f18f-51c62182a951.png)


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

![スクリーンショット 2019-10-21 16.11.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/07fd60b6-2375-6ded-210c-c2fa3598000c.png)

#### タスク定義の作成

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

![スクリーンショット 2019-10-21 14.12.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/e9bbff15-66e3-c22c-4595-992033848aeb.png)

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

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

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

![スクリーンショット 2019-10-21 14.19.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/717a2397-1e54-6f2a-6016-630dd43a572e.png)

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

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

![スクリーンショット 2019-10-21 14.33.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/f9aacd4b-96cf-6700-d9b4-2b97d93cf739.png)

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

![スクリーンショット 2019-10-21 14.38.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/16f04073-2cdf-6992-61a0-18c97c2a55ba.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/3f6f0166-2070-b3be-f982-06f5e50fd9fe.png)

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

#### ステップ3 Auto Scaling(オプション)
ステップ3はAutoScaleingの設定です。
商用で利用する場合は必ず設定しましょう。

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

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

![スクリーンショット 2019-10-21 15.12.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/501dbfee-9691-c84d-6012-94b8e06d45b2.png)

#### APIGatewayの作成
APIGatewayを作成します。

![スクリーンショット 2019-10-21 15.36.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/dd385337-ca77-e6e5-7136-ad4844636301.png)

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

![スクリーンショット 2019-10-21 16.03.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/fec5363e-368f-64b4-e59b-5b45f766eef1.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/b9846d7e-fa8a-db80-fe7c-5d1a5846963e.png)

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

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

![スクリーンショット 2019-10-23 12.19.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/21c46c76-0867-008a-022d-2a02c403ca79.png)

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

![スクリーンショット 2019-10-23 12.28.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/60726432-6572-762c-9d2b-4ae5a81c1725.png)

```json
{
  "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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/298b8180-6416-8f62-b618-e9c1e293f1b2.png)

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

![スクリーンショット 2019-10-21 17.55.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/b23b8794-6df3-04e5-2411-63f82e70dbb4.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/790b2932-ef29-f1e3-6e1e-cecf41e6943d.png)

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

![スクリーンショット 2019-10-23 11.28.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/1707ff17-913f-4ec9-39b7-942f3aefca08.png)

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

![スクリーンショット 2019-10-23 11.30.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/466431/54932d30-c22b-07f9-9413-2a56ad9dc0d2.png)

### APIGatewayのデプロイ
APIGatewayについても、今後手動で設定していくのが辛いのでCI環境内でデプロイしたいものです。
今回は、CodeBuild上でデプロイします。
`buildspec.yaml`の`post_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"
```

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