LIFULL Creators Blog

LIFULL Creators Blogとは、株式会社LIFULLの社員が記事を共有するブログです。自分の役立つ経験や知識を広めることで世界をもっとFULLにしていきます。

AWS Fargate, Github Actionsを利用したウェブアプリケーション開発から配布まで

はじめ

こんにちは、アプリケーションエンジニアとして働いてます。キム ソンジュです。 今回の記事では自分が参加したPJで利用した、インフラ構成から、CI/CD環境を利用して簡単にアプリケーション開発ができる方法について紹介しようと思います。

システム投入・設計背景

既存のレガシーシステムには、次の問題がありました。

  • デプロイの手順が複雑で時間かかり、面倒な作業が多い
  • 環境ごとにミドルウェアのバージョンが異なる

この問題を解決し、かつ新しい技術にチャレンジするために、チーム内で次の内容で進めるようチームで決めました。

  • Dockerを活用して環境ごとの差をなくす
  • GitHubでソースコードを管理するので、CI/CDにはGitHub Actionsを採用
  • Dockerを利用することによる、ECRとECSを活用

入る前に

本記事で話したい内容は「このような方法で、こんなに簡単にアプリの開発からデプロイまでできる」といった紹介をするのが目的です。 案内してるAWSの各技術の細かい使い方、テストで使ってるGo言語の深い話については紹介しませんのでその点をご了承ください。

登場人物

  • AWS IAM
  • AWS ECR
  • AWS ECS
  • AWS ALB(※1)
  • AWS Parameter Store(※2)
  • Github Actions
  • Docker
  • Go ( Go以外のアプリケーションも利用可能です)

※1. ALBはFargateを利用する時の必須技術になります。
※2. Parameter Storeは、Dockerを起動させる時に環境変数を利用して敏感な情報をアプリで利用するため使います。
※その他の登場人物については、上記で決めたことを元にして登場しました。

作業結果の予想図

上記の登場人物を利用する場合、下記のような構成が出ます。
f:id:LIFULL-SeongjooKIM:20200909172517p:plain

作業の流れ

  1. Goを利用したアプリ作成
  2. Dockerを利用したContainer環境構築
  3. Local環境起動確認・テスト
  4. AWS設定
  5. Github Actions設定
  6. 実装テスト

こちらの流れで作業を進めて行く予定です。 それでは始めましょう!

1. Goを利用したアプリ作成

  • go-json-resetのModuleを利用します。
    • この記事では上記の例文をそのまま利用する予定です。
  • go moduleを利用します。
  • 参考コードはこちらです。

2. Dockerを利用したContainer環境構築

  • goが設定されてるAlphine Linuxを利用します。
  • TimeZone設定がDefault UTCなので調整します(ログ書くときに時間がずれることを予防します)
  • go moduleの設定と、Install作業を行います。
  • buildした後、実行させます。
dockerfile
## get alphine + go image ver.1.14
FROM golang:1.14.2-alpine3.11

## pakcage update & install
RUN apk add --update curl git pkgconfig curl-dev 

## modified timezone
RUN apk add tzdata
ENV TZ=Asia/Tokyo

## setup env variables for go module
ENV GO111MODULE "on"

## source code copy from host disk
WORKDIR /go
COPY . /go/src

## module install
## If You didn't set up go.mod in your local workspace, You can not use command go install
WORKDIR /go/src
RUN go install

## go build
RUN go build ./main.go

## container listen port
EXPOSE 8080

## command
## start
CMD ["./main"]

ここまで来たら下記のようなディレクトリ構成になります。

.
├── Dockerfile
├── go.mod // go module 設定ファイルになります。
├── go.sum // なくても構いません。
└── main.go

3. Local環境起動確認・テスト

  • docker build
  • postman, curl などを利用して、テスト
docker build -t lifull_test .
docker run -p 8080:8080 qiita_test:latest

自分はPostmanを利用して見ました。 f:id:LIFULL-SeongjooKIM:20200910120312p:plain アクセスログが出力されているか見てみましょう?! f:id:LIFULL-SeongjooKIM:20200910120332p:plain よし、ここまで来たら準備は完了です。

4. AWS設定

AWSの環境を設定しましょう、ALB,SGの細かい設定についてはこの記事では案内しません、必要最低限の設定で作成します

  • IAM 設定

    • Github Actionsが利用するDeploy用ユーザーを作成
      • 必要権限は ECR,ECSに関連する権限を付与します。
      • 作成時 AccesToken、Keyを保存しましょう。 f:id:LIFULL-SeongjooKIM:20200910120729p:plain
    • FargateのTaskが使うRoleを設定します。
      • ecsTaskExecutionRole この名がDefaultです。
      • 権限は、ECR,ECSの権限に更に、CloudWatch,Parameter Storeへの利用権限が必要です。 f:id:LIFULL-SeongjooKIM:20200910120819p:plain
  • Parameter Store 設定

    • アプリで利用する各種環境変数周りがあります(Laravelの場合.env周りに設定する環境変数です)こちらをGo+Dockerで利用する場合、環境変数をOSで扱うのが一般的で、こちらを実現するためにAWSでは SystemManager > Parameter Storeに設定します。

    • key-valueで設定します。暗号化された文字列として保存するのがより安全ですね!

  • Security Group 作成
    • 利用するポートを許可します。 今回の場合は8080をOpenしましょう!
  • ALB 作成
    • Public Subetを設定します。
    • Public DNSが使える状態にします。もしくはHTTPS設定を利用して使える場合DNS設定を行います。(ACMなどが登場しますね)
  • ECR 作成
    • Repositoryを作成します。(Docker-hubのPrivate版的な感じで、AWSでDockerを利用する時よく使います。)
  • ECS 設定
    • ECSの構成について細かい説明は行いません。下記の設定は注意してください。
    • task定義が必要です。
      • まだRepositoryにアップしたイメージがないので適当に書いておいても構いません。  
      • Containerに渡す環境変数の設定が必要です。上記のParameter Storeで設定したパラメーター名を環境変数:パラメーター名構成で作成します。
      • Container側に必要はPortをOpenする必要もあります。ここでは8080ですね。
    • clusterを作成します。
      • serviceを作成します。

5. Github Actions設定

一覧の設定が終わったら、Github Actionsを設定しましょう
  • Github Actionsについては気になる方は以前自分がQiitaに投稿した記事がありますので、参考にしてください

  • Trigger

    • push
  • Target Branch
    • master
  • doing
    • docker build
    • task-definition.json を利用したDeploy作業
.github/workflow/main.yml
### main.yml
name: test workflow for qiita
on:
  push:                             # event trigger on push
    branches: master

jobs:
  build:                             # job id
    name: sjkim action               # job name
    runs-on: ubuntu-latest           # virtual os
    steps:
      - name: set up go 1.14
        uses: actions/setup-go@v1
        with:
          go-version: 1.14

      - name: Checkout branch go module directory
        uses: actions/checkout@v2

      - name: package install
        uses: actions/cache@v2
        with:
          path: ~/go/pkg/mod
          key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
          restore-keys: |
            ${{ runner.os }}-go-

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: qiita-test
          IMAGE_TAG: ${{ github.sha }}
        run: |
           # Build a docker container and
           # push it to ECR so that it can
           # be deployed to ECS.
           docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
           docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
           echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

      - name: Fill in the new image ID in the Amazon ECS task definition
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: task-definition.json
          container-name: qiita-container
          image: ${{ steps.build-image.outputs.image }}

      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          service: qiita-service
          cluster: qiita-test-cluster
          wait-for-service-stability: true

Github Actions marketplaceにあるECS Deployを元に作成してます。

Github Secretsについて

Github Actions上で利用する秘密情報や、アクセストークンなどを利用する時に使います。

  • {{ secrets.AWS_SECRET_ACCESS_KEY }} などで利用可能です。
  • setting > secrets > new secrets f:id:LIFULL-SeongjooKIM:20200910122744p:plain
task-definition.json
{
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "inferenceAccelerators": [],
  "containerDefinitions": [
    {
      "name": "qiita-container",
      "image": "***/qiita-test",
      "resourceRequirements": null,
      "essential": true,
      "portMappings": [
        {
          "hostPort": 8080,
          "containerPort": "8080",
          "protocol": "tcp"
        }
      ],
      "secrets": [
        {
          "name": "ENV",
          "valueFrom": "dev"
        }
      ],
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/qiita-test",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "qiita"
        }
      }
    }
  ],
  "volumes": [],
  "networkMode": "awsvpc",
  "memory": "512",
  "cpu": "256",
  "executionRoleArn": "****/ecsTaskExecutionRole",
  "family": "qiita-task",
  "taskRoleArn": "",
  "placementConstraints": []
}

taskは上記AWSの作業手順のなか、Taskを作成したら、Task詳細タブの中にJsonて書かれてる部分があります。 その部分をコピしてLocalのPJ内に配置したらOKです。

  • Cloud Watchのロググループは存在してない場合、自動的に作成してくれます。(Deployユーザーに権限が無い場合Deploy中にNGになりますので注意)
  • secretsの部分でContainerにわたす環境変数を設定します。nameは渡す環境変数名 valueFromはParameter Storeに指定した名前を入れます。
  • container name, task name, cluster name, service name など、先に設定しておかない、又はタイポで間違った内容を書くと、Github Actionsの動作中に落ちます。

6. テスト

もうほぼ準備は完了です。 ここまでのPJの構成が下記になります。

├── .github
│   └── workflows
│       └── main.yml
├── Dockerfile
├── go.mod
├── go.sum
├── main.go
└── task-definition.json

実装をしてみましょう!

  • GithubのMasterブランチへPushしたら、Github Actionsが起動されます。
  • その流れによって、ECSにデプロイ作業が開始されます。
  • 作業が終わったら、AWS管理ページを確認し、EC2>ターゲットグループチェックします。
  • Public DNSや、利用してるDNSを利用してAPIが叩けるのか確認します!

まとめ

今回このような構成でアプリケーションの開発を行ったのは初の試みです。 AWSの知識が浅かったのでかなり苦労した記憶が残ってますが、かなりいい構成かな…と思ってるのでこちらをベースにPJをどんどん拡張していきたいなと思います。 Firelensなどを利用したLog設定や、Github Actions、AWS SNSを利用したアラーム設定、Containerのリソースモニタリングなど、どんどん入れてみたいなと思ってます。

おそらくこの記事の情報だけではそんなに簡単にアプリを作るのは難しいかもしれません。 でもこの記事の内容を参考にしてもらい、皆さんのPJや、悩みへ少しでもHINTになったら嬉しいです。

本日案内したコードはここを参考してください。