3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AWS CDKとGitHub ActionsでVueのWebページを環境別デプロイ

Last updated at Posted at 2020-07-26

このディレクトリ構成で、AWSへのデプロイフローを作る。
Vue.jsのWebサイトを 本番 (prod) / ステージング (stg)環境に分けて管理する。ステージング環境は検証用として使える。

v2okimochi-dev/
├ .gitub/
│ └ workflows/
│   └ cdk.yml (GitHub Actionsワークフローを定義するymlファイルを置く)
│
├ frontend/
│ ├ staging_assets/
│ │ └ robots.txt
│ └ ... (ここにVueのアレコレを置く)
│
├ batch/
│ └ signed_url/
│   └ ... (ここに証明書付きURL作成用のPythonバッチアレコレを置く)
│
└ infra/
  └ aws-cdk/
    └ ... (ここにAWS CDKのアレコレを置く)

ほとんどのAWSサービスは AWS CDK (docsはコレ)でデプロイする。CDKの性質上、CloudFormationを軸に管理されるみたい。

CDKによるデプロイ部分は、 (local上でも良いが、) GitHub Actionsに定義して developブランチにpush (merge)されたらステージング環境へデプロイし、masterブランチにpush (merge)されたら本番環境へデプロイするような自動デプロイフローを作る。

全体としては、こうなる

image.png

  • S3
    • frontend/dist/を置く場所
    • CloudFront以外からアクセスさせる気はないのでOrigin Access Identityだけ許す
    • ステージング環境だけ robots.txtも置く
      • 念のため検索ロボットによる捕捉も防ぐ
  • CloudFront
    • S3内のファイルは全てCloudFrontを通してアクセスさせる
    • HTTPS通信だけ許す
    • 本番環境だけ、予め取得しておいたAWSのパブリック SSL/TLS証明書&独自のドメイン名を設定する
      • 詳細はそれぞれRoute53, Certificate Managerの項目に書いた
      • デフォルトではAWSによってCloudFront用のドメイン名と証明書が付与される
    • ステージング環境だけ、 index.htmlへのアクセスは署名付きURL認証を差し込む (公開されても嬉しくないので)
      • 全ファイルに差し込みたかったが index.html以外が403を返してしまい、原因もわからず泣く泣く妥協
      • index.html以外のファイルはURLがバレれば未認証でもアクセスできてしまうのがだいぶアレ
  • Route53
    • 本番環境だけ、独自ドメインを含むドメイン名 (たとえば www.mydomain.com)でCloudFrontへ繋ぐ
    • 今回は お名前.com (外部)で登録したドメイン名なので、 お名前.com側に 予めRoute53で作ったホストゾーン内のネームサーバーを登録しておく
    • CDKではAレコード・AAAAレコードの作成をやる (FQDNとCloudFrontドメイン名の紐付け)
  • Certificate Manager
    • AWSのパブリック SSL/TLS証明書を取得する
    • リージョンは バージニア北部 (us-east-1)で作成する (CloudFrontとの結びつけがここしか対応してなさそう)
    • 予めRoute53に CNAMEレコードとして登録しておく

AWS側で予めやっておく手動作業

全部cdkでやりたかったけど。。。 :thinking: :thinking: :thinking:

AWS外で登録した独自ドメインをAWS Route53で管理

たとえば お名前.comで独自ドメイン mydomain.comを登録した場合。

  1. Route53でパブリックホストゾーン mydomain.comを作成
    このホストゾーンIDは後で使うのでメモしておく
  2. お名前.comで独自ドメイン mydomain.comのネームサーバーを、 "1.によりホストゾーンに自動生成された NSレコード mydomain.comのネームサーバー"に変更

Route53でメモするネームサーバーはこのへん
image.png

お名前.comで変更するネームサーバーはこのへん
image.png

AWSでSSL/TLS証明書を取得する

  1. Certificate Managerでパブリック証明書 *.mydomain.comをリクエストする
    リクエストが作られ、ステータスが 検証保留中となるはず
  2. 証明書のCNAMEレコードをRoute53のホストゾーン mydomain.comに追加する
    ( Route53に追加みたいなボタンを押せば自動で追加してくれる)
  3. 証明書のARNをメモしておく (CloudFrontとの紐付けをcdkでやるため)

local上での作業

shellで手作業

CDKの導入は Getting started with the AWS CDKに従った。

あとCDKは全部TypeScriptで書いた。

必要なライブラリのインストール
npm install @aws-cdk/aws-s3 @aws-cdk/aws-s3-deployment @aws-cdk/aws-cloudfront @aws-cdk/aws-certificatemanager @aws-cdk/aws-route53 @aws-cdk/aws-route53-targets

AWSアカウントIDとリージョンを指定して CDKTookKitというCloudFormation スタックを作成

cdk bootstrap XXXXXXXXXXXX/ap-northeast-1

AWS CDKで利用するみたい。作成してないと、こういうエラーを吐かれる。。。 :innocent:

エラー文
❌  v2okimochi-dev failed: Error: This stack uses assets, so the toolkit stack must be deployed to the environment (Run "cdk bootstrap aws://unknown-account/unknown-region")

cf. AWS CDKの'aws-s3-deployment'を使ってクライアントサイドも一緒にデプロイする

AWS CDKのstackを記述

各ソースコード (いっぱいあるので折りたたんだ)
aws-cdk/bin/aws-cdk.ts
# !/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { AwsCdkStack } from '../lib/stacks/v2okimochi-dev';
import * as util from "../lib/util";

const validateEnvironment = (target: string) => {
  /**
   * 意図した環境が `--context`オプションで指定されていない場合、例外終了させる
   */

  if (!target) throw new Error("環境が指定されていません");

  const validTarget = util.findEnvironment(target);
  if (!validTarget) throw new Error(`環境名が正しくありません: ${target}`);

  return validTarget;
};

const productName = "v2okimochi-dev";

const app = new cdk.App();
const target = validateEnvironment(app.node.tryGetContext("target"))

new AwsCdkStack(app, `${productName}-${target}`, target);
aws-cdk/lib/util.ts
/**
 * プロダクト環境名の定義
 */

export enum Environments {
  PROD = "prod",
  STG = "stg",
}

export function findEnvironment(env: string): string | undefined {
  if (env == Environments.PROD) return Environments.PROD;
  else if (env == Environments.STG) return Environments.STG;
  else return undefined;
}

/**
 * 環境変数の取り出し
 */

export function validateEnvironmentVariable(key: string): string {
  const value = process.env[key];
  if (!value) throw new Error(`環境変数の値が見つかりません: ${key}`);
  return value;
}
aws-cdk/lib/stacks/v2okimochi-dev.ts
import * as cdk from "@aws-cdk/core";
import * as certificatemanager from "@aws-cdk/aws-certificatemanager";
import * as s3 from "@aws-cdk/aws-s3";
import * as s3deploy from "@aws-cdk/aws-s3-deployment";
import { Environments, validateEnvironmentVariable } from "../util";
import { setupCloudFront } from "../services/cloudfront/setupCloudFront";
import { setupRoute53 } from "../services/route53/setupRoute53";

export class AwsCdkStack extends cdk.Stack {
  constructor(
    scope: cdk.Construct,
    id: string,
    target: string,
    props?: cdk.StackProps
  ) {
    super(scope, id, props);

    /** ##############################
     * S3 Bucket
     */

    const s3WebName = `${id}-web`;
    const s3Web = new s3.Bucket(this, id, {
      bucketName: s3WebName,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // Prod環境以外では、Webクローラに登録させないためのrobots.txtもデプロイする
    const s3WebAssets: s3deploy.ISource[] =
      target === Environments.PROD
        ? [s3deploy.Source.asset("../../frontend/dist")]
        : [
            s3deploy.Source.asset("../../frontend/dist"),
            s3deploy.Source.asset("../../frontend/staging_assets"),
          ];
    new s3deploy.BucketDeployment(this, `${s3WebName}-deployment`, {
      sources: s3WebAssets,
      destinationBucket: s3Web,
    });

    /** ##############################
     * CloudFront, Certificate Manager
     */

    const validDomain = validateEnvironmentVariable(
      "V2OKIMOCHI_DEV_PROD_DOMAIN"
    );
    const validSubDomain = validateEnvironmentVariable(
      "V2OKIMOCHI_DEV_PROD_SUBDOMAIN"
    );
    const fqdn = `${validSubDomain}.${validDomain}`;
    const arn = validateEnvironmentVariable(
      "V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN"
    );
    const validCertificate = certificatemanager.Certificate.fromCertificateArn(
      this,
      `${id}-acm-certificate`,
      arn
    );

    const cloudFront = new setupCloudFront(this, {
      id: `${id}-cloudfront`,
      target: target,
      s3WebBucket: s3Web,
      certificate: validCertificate,
      fqdns: [fqdn],
    });

    /** ##############################
     * Route53
     */

    const route53 =
      target === Environments.PROD
        ? new setupRoute53(this, {
            id: `${id}-route53`,
            target: target,
            domain: validDomain,
            subDomain: validSubDomain,
            cloudFront: cloudFront,
          })
        : undefined;

    /** ##############################
     * Tag
     */

    cdk.Tag.add(this, "Product", id);
  }
}
aws-cdk/lib/services/cloudfront/setupCloudFront.ts
import * as cdk from "@aws-cdk/core";
import * as certificatemanager from "@aws-cdk/aws-certificatemanager";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as s3 from "@aws-cdk/aws-s3";
import { Environments, validateEnvironmentVariable } from "../../util";

export interface setupCloudFrontProps {
  /**
   * リソースの共通名 (たとえばプロダクト名)
   */
  readonly id: string;
  /**
   * 環境 (Prodなど)
   */
  readonly target: string;
  /**
   * オリジンとして指定するS3 Bucket
   */
  readonly s3WebBucket: s3.Bucket;
  /**
   * SSL/TLS証明書を取得したAWS Certificate Manager
   */
  readonly certificate: certificatemanager.ICertificate;
  /**
   * CloudFrontのカスタムFQDNリスト
   */
  readonly fqdns: string[];
}

export class setupCloudFront {
  private cloudFrontWebDistribution: cloudfront.CloudFrontWebDistribution;
  public distribution(): cloudfront.CloudFrontWebDistribution {
    return this.cloudFrontWebDistribution;
  }

  constructor(context: cdk.Stack, props: setupCloudFrontProps) {
    /** ##############################
     * 本番 (Prod)以外の環境では、公開しないようにデプロイする
     */

    const signedAccount = validateEnvironmentVariable(
      "V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID"
    );

    // Prod環境以外では署名付きURLを使用する
    const behaviorsWithEnv: cloudfront.Behavior[] =
      props.target === Environments.PROD
        ? [
            {
              isDefaultBehavior: true,
              compress: true,
              minTtl: cdk.Duration.seconds(0),
              maxTtl: cdk.Duration.days(0),
              defaultTtl: cdk.Duration.days(0),
            },
          ]
        : [
            {
              isDefaultBehavior: false,
              pathPattern: "/index.html",
              trustedSigners: [signedAccount],
              compress: true,
              minTtl: cdk.Duration.seconds(0),
              maxTtl: cdk.Duration.days(0),
              defaultTtl: cdk.Duration.days(0),
            },
            {
              isDefaultBehavior: true,
              compress: true,
              minTtl: cdk.Duration.seconds(0),
              maxTtl: cdk.Duration.days(0),
              defaultTtl: cdk.Duration.days(0),
            },
          ];

    // Prod環境以外では403を200にリダイレクトさせる
    const errorConfigurationsWithEnv: cloudfront.CfnDistribution.CustomErrorResponseProperty[] =
      props.target === Environments.PROD
        ? [
            {
              errorCode: 403,
              errorCachingMinTtl: 0,
              responseCode: 200,
              responsePagePath: "/index.html",
            },
          ]
        : [];

    // Prod環境にだけ独自ドメイン名を割り当てる
    const viewerCertificateWithEnv: cloudfront.ViewerCertificate | undefined =
      props.target === Environments.PROD
        ? {
            aliases: props.fqdns,
            props: {
              acmCertificateArn: props.certificate.certificateArn,
              sslSupportMethod: "sni-only",
            },
          }
        : undefined;

    // CloudFrontだけがS3にアクセスできるようにする (ユーザはS3に直接アクセスできない)
    const oai = new cloudfront.OriginAccessIdentity(
      context,
      `${props.id}-oai`,
      {
        comment: "s3 access.",
      }
    );

    this.cloudFrontWebDistribution = new cloudfront.CloudFrontWebDistribution(
      context,
      `${props.id}-cloudfront`,
      {
        defaultRootObject: "index.html",
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
        viewerCertificate: viewerCertificateWithEnv,
        httpVersion: cloudfront.HttpVersion.HTTP2,
        priceClass: cloudfront.PriceClass.PRICE_CLASS_200,
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: props.s3WebBucket,
              originAccessIdentity: oai,
            },
            behaviors: behaviorsWithEnv,
          },
        ],
        geoRestriction: {
          restrictionType: "whitelist",
          locations: ["JP"],
        },
        errorConfigurations: errorConfigurationsWithEnv,
      }
    );
  }
}
aws-cdk/lib/services/route53/setupRoute53.ts
import * as cdk from "@aws-cdk/core";
import * as route53 from "@aws-cdk/aws-route53";
import * as route53targets from "@aws-cdk/aws-route53-targets";
import { setupCloudFront } from "../../services/cloudfront/setupCloudFront";
import { validateEnvironmentVariable } from "../../util";

export interface setupRoute53Props {
  /**
   * リソースの共通名 (たとえばプロダクト名)
   */
  readonly id: string;
  /**
   * 環境 (Prodなど)
   */
  readonly target: string;
  /**
   * ドメイン名
   */
  readonly domain: string;
  /**
   * サブドメイン名
   */
  readonly subDomain: string;
  /**
   * CloudFront distribution
   */
  readonly cloudFront: setupCloudFront;
}

export class setupRoute53 {
  constructor(context: cdk.Stack, props: setupRoute53Props) {
    const fqdn = `${props.subDomain}.${props.domain}`;
    const hostedZoneId = validateEnvironmentVariable(
      "V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID"
    );

    const hostedZone = route53.HostedZone.fromHostedZoneAttributes(
      context,
      `${props.id}-hosted-zone`,
      {
        zoneName: props.domain,
        hostedZoneId: hostedZoneId,
      }
    );

    const aRecord = new route53.ARecord(context, `${props.id}-a-record`, {
      zone: hostedZone,
      recordName: fqdn,
      target: route53.RecordTarget.fromAlias(
        new route53targets.CloudFrontTarget(props.cloudFront.distribution())
      ),
    });

    const aaaaRecord = new route53.AaaaRecord(
      context,
      `${props.id}-aaaa-record`,
      {
        zone: hostedZone,
        recordName: fqdn,
        target: route53.RecordTarget.fromAlias(
          new route53targets.CloudFrontTarget(props.cloudFront.distribution())
        ),
      }
    );
  }
}

アカウントIDとか、ハードコーディングするとアレなやつは環境変数として埋め込むようにした。

  • V2OKIMOCHI_DEV_PROD_DOMAIN
    • 本番環境で使う独自ドメイン
    • 今回は mydomain.comみたいなやつ
  • V2OKIMOCHI_DEV_PROD_SUBDOMAIN
    • 本番環境で使うサブドメイン
    • たとえば www (FQDNにすれば www.mydomain.comになる)
  • V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID
    • 予め作っておいたRoute53ホストゾーンのID
  • V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN
    • 予め作っておいたCertificate Manager SSL/TLS証明書のARN
  • V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID
    • 署名済みユーザとして登録するアカウントID
    • 今回は自分のアカウントIDだけ

たとえばこうやってCDKコマンドを使う。
diffで差分と文法エラーを確認、deployで実際にデプロイ、destroyで該当スタックを削除する。

aws-cdk/
# 本番
$ cdk diff --context target=prod

# ステージング
$ cdk diff --context target=stg

実行時の引数でスタックIDごと変えることによって、本番環境とステージング環境を分けるようにした。

S3の設定

aws-cdk/lib/stacks/v2okimochi-dev.tsに書いた。

CloudFrontの設定

aws-cdk/lib/services/cloudfront/setupCloudFront.tsに書いた。
本番/ステージングの環境ごとに設定を用意したりして長くなったのでファイルを分けている。

  • SSL/TLS証明書を予め手作業で (Certificate Managerに)作っておき、そのARNからCDKで参照する
  • trustedSignersにアカウントIDを指定することで、署名付きURLでのアクセス時にそのアカウントIDで持っているCloudFrontキーペアで照合されるっぽい
    • 自分のアカウントIDだけ指定した場合、CloudFront管理画面上では selfと表示される
  • Route53のレコードでも紐付けるが、CloudFront側にもaliasとして独自ドメイン名を付ける ( CNAMEs列に表示される)
  • なんとなく日本だけからアクセスできるようにした (geoRestriction)

Route53の設定

aws-cdk/lib/services/route53/setupRoute53.tsに書いた。
本番環境でだけ作成する (条件分岐は aws-cdk/lib/stacks/v2okimochi-dev.tsのほう)

  • ホストゾーンは予め手作業で作っておき、そのID ( HostedZoneId)からCDKで参照する
  • CDKで作るのはAレコードとAAAAレコード
  • どちらもCloudFrontディストリビューションと独自ドメイン名を紐付ける
    • ディストリビューションはCDK内で作成したインスタンスから拾う
    • もしCDKでやらずにWebから操作するなら、紐付け対象を CloudFront配信、リージョンを バージニア北部にしてCloudFrontドメイン名のほうを貼り付ければ良い (選択候補には出ないので手動で貼り付ける)

Tagの設定

aws-cdk/lib/stacks/v2okimochi-dev.tsの最後にしれっと書いている。
どうせCloudFormationによって管理されてるけど。。。

署名付きURLの作成

(手動だけど。。。)

/batch/signed_url/に、 ステージング環境のWebページにアクセスするための署名付きURLを作成するPythonスクリプトを書いた。

そんな頻繁に定期実行するわけじゃないからバッチと呼ぶことには諸説ありそうだが。。。

AWS側での事前準備

署名済みユーザ (今回は自分のアカウントID)でCloudFrontキーペアを作り、プライベートキー ( .pem)をダウンロードする。
rootユーザとしてログインしないと作れないみたい。

cf. 署名付き URL と署名付き Cookie (信頼された署名者) の作成が可能な AWS アカウントの指定

スクリプト作成と実行

pipenvで適当にpython3.8の仮想環境を整えて、 boto3をimportして使う。

signed_url/Pipfile
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[packages]
boto3 = "==1.4.4"

[dev-packages]
flake8 = "==3.8.3"

[requires]
python_version = "3.8.3"

[scripts]
flake8 = "flake8 --ignore E501 ."

([dev-packages]と[scripts]に関する記述はAWSとは無関係なので無くても良い)

公式docsに従い、署名付きURL生成のコードを書く。

signed_url/src/main.py
import datetime
import os

from botocore.signers import CloudFrontSigner
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding


def rsa_signer(message):
    private_pem_path = os.getenv(
        'V2OKIMOCHI_DEV_STG_CLOUDFRONT_PRIVATE_PEM_PATH')
    with open(private_pem_path, 'rb') as key_file:
        private_key = serialization.load_pem_private_key(
            key_file.read(), password=None, backend=default_backend())
    return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())


def handler():
    key_id = os.getenv('V2OKIMOCHI_DEV_STG_CLOUDFRONT_KEY_ID')
    url = os.getenv('V2OKIMOCHI_DEV_STG_CLOUDFRONT_URL')
    expire_date = datetime.datetime(2020, 7, 28)

    cloudfront_signer = CloudFrontSigner(key_id, rsa_signer)

    # Create a signed url that will be valid until the specfic expiry date
    # provided using a canned policy.
    signed_url = cloudfront_signer.generate_presigned_url(
        url, date_less_than=expire_date)
    print(signed_url)


if __name__ == "__main__":
    handler()

cf. Boto3 Docs - Generate a signed URL for Amazon CloudFront

ハードコーディングがアレな値は環境変数にした。

  • V2OKIMOCHI_DEV_STG_CLOUDFRONT_PRIVATE_PEM_PATH
    • ダウンロードしたCloudFrontプライベートキー ( .pem)へのパス
    • たとえば /batch/signed_url/に置いたなら値は ./xxxxx.pemみたいになる
  • V2OKIMOCHI_DEV_STG_CLOUDFRONT_KEY_ID
    • pemファイル名に含まれているkey ID (大文字で20字くらいあるアレ)
  • V2OKIMOCHI_DEV_STG_CLOUDFRONT_URL
    • https://xxxxxxxxxxx.cloudfront.net/index.htmlみたいになる

スクリプトで署名付きURLを発行

この main.pyを実行して、署名付きURLを発行する。
今回はpipenvでPython仮想環境を作ったので pipenv runでやる

/batch/signed_url/
$ pipenv run python src/main.py
  • 上記コードでは 2020/7/28まで有効なURLを発行する
  • print文で標準出力されたURLを (改行は取り除いて)使う
  • このへんもガチで自動化しようとするなら、コードをLambda化してCognitoAPI Gatewayと連携させてログインページ作るくらいのことが必要そう
    • Cookie付与とかもしんどかったので今回はクエリパラメータだけで済む署名付きURLにした

GitHub Actionsの記述

公式docsに従い、GitHubリポジトリに /.github/workflows/ディレクトリを用意してyamlで定義する。

cf. https://docs.github.com/ja/actions/configuring-and-managing-workflows/configuring-a-workflow

動きとしてはこんな感じ

  • test_and_build_frontend
    • /frontend/でlintして問題なければbuild
    • (テストがあればtestもやるべきだけど今回はテスト書いてない :innocent: )
  • infra
    • 前のjobが成功してから動くように needsで依存関係を作った
    • /infra/aws-cdk/でdiffして問題なければdeploy
    • masterブランチ以外へのpushなら、ステージング環境へのdiffまでやる
    • developブランチへのpushなら、ステージング環境へのdiffとdeployまでやる
    • masterブランチへのpushなら、本番環境へのdiffとdeployまでやる
ソースコード (長いので折りたたんだ)
/.github/workflows/cdk.yml
name: cdk

on: push
env:
  production-branch: 'refs/heads/master'
  staging-branch: 'refs/heads/develop'
  frontend-working-directory: frontend
  infra-working-directory: infra/aws-cdk
  aws-default-region: 'ap-northeast-1'
jobs:
  test_and_build_frontend:
    runs-on: ubuntu-18.04
    defaults:
      run:
        shell: bash
        working-directory: frontend
    timeout-minutes: 5
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Cache frontend Node modules
        uses: actions/cache@v2
        env:
          cache-name: frontend-cache-node-modules
        with:
          # npm cache files are stored in `~/.npm` on Linux/macOS
          path: ~/.npm
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-
      
      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: '12.18'
      
      - name: Setup dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Build
        run: npm run build

      - name: Upload frontend built path
        uses: actions/upload-artifact@v2
        with:
          name: frontend-build-path
          path: frontend/dist

  infra:
    needs: [test_and_build_frontend]
    runs-on: ubuntu-18.04
    defaults:
      run:
        shell: bash
        working-directory: infra/aws-cdk
    timeout-minutes: 10
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Cache infra Node modules
        uses: actions/cache@v2
        env:
          cache-name: infra-cache-node-modules
        with:
          # npm cache files are stored in `~/.npm` on Linux/macOS
          path: ~/.npm
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-build-${{ env.cache-name }}-
            ${{ runner.os }}-build-
            ${{ runner.os }}-

      - name: Setup Node
        uses: actions/setup-node@v1
        with:
          node-version: '12.18'

      - name: Setup dependencies
        run: npm ci

      - name: Download frontend built path
        uses: actions/download-artifact@v2
        with:
          name: frontend-build-path
          path: frontend/dist

      - name: CDK Diff (Validate) with Stg
        if: ${{github.ref != env.production-branch }}
        run: npm run cdk diff -- --context target=stg
        env:
          AWS_DEFAULT_REGION: ${{ env.aws-default-region }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          V2OKIMOCHI_DEV_PROD_DOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_DOMAIN }}
          V2OKIMOCHI_DEV_PROD_SUBDOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_SUBDOMAIN }}
          V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID: ${{ secrets.V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID }}
          V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN: ${{ secrets.V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN }}
          V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID: ${{ secrets.V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID }}

      - name: CDK Diff (Validate) with Prod
        if: ${{github.ref == env.production-branch }}
        run: npm run cdk diff -- --context target=prod
        env:
          AWS_DEFAULT_REGION: ${{ env.aws-default-region }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          V2OKIMOCHI_DEV_PROD_DOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_DOMAIN }}
          V2OKIMOCHI_DEV_PROD_SUBDOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_SUBDOMAIN }}
          V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID: ${{ secrets.V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID }}
          V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN: ${{ secrets.V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN }}
          V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID: ${{ secrets.V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID }}

      - name: CDK Deploy to Stg
        if: ${{github.ref == env.staging-branch }}
        run: npm run cdk deploy -- --context target=stg --require-approval never
        env:
          AWS_DEFAULT_REGION: ${{ env.aws-default-region }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          V2OKIMOCHI_DEV_PROD_DOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_DOMAIN }}
          V2OKIMOCHI_DEV_PROD_SUBDOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_SUBDOMAIN }}
          V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID: ${{ secrets.V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID }}
          V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN: ${{ secrets.V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN }}
          V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID: ${{ secrets.V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID }}

      - name: CDK Deploy to Prod
        if: ${{github.ref == env.production-branch }}
        run: npm run cdk deploy -- --context target=prod --require-approval never
        env:
          AWS_DEFAULT_REGION: ${{ env.aws-default-region }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          V2OKIMOCHI_DEV_PROD_DOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_DOMAIN }}
          V2OKIMOCHI_DEV_PROD_SUBDOMAIN: ${{ secrets.V2OKIMOCHI_DEV_PROD_SUBDOMAIN }}
          V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID: ${{ secrets.V2OKIMOCHI_DEV_PROD_HOSTED_ZONE_ID }}
          V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN: ${{ secrets.V2OKIMOCHI_DEV_PROD_CERTIFICATE_ARN }}
          V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID: ${{ secrets.V2OKIMOCHI_DEV_SIGNED_ACCOUNT_ID }}

  • npm run buildした成果物をupload保存しておき、後のjobでdownloadしてデプロイする
  • ブランチ名は github.refで取得できるみたい
  • npm runのコマンドに引数を渡す際は --を間に差し込む
    cf. https://docs.npmjs.com/cli/run-script
  • 今回cdkコマンドには引数として targetを渡す (環境を判別する)
    • 独自に引数を渡すには --context
  • cdk deployには --require-approval neverも加える
    • IAM roleなどセキュリティ関係の変更もスッと通す (デフォルトだと確認が差し込まれてしまい yでEnterせねばならない)
  • cdkのコードで利用する環境変数は、GitHub側のsecretsに定義した上でworkflow上の envに紐付けておく (今回はstepごとに定めた)
  • timeout-minutesを仕込んだ (cdkでたまに無限デプロイを始めてしまうので)

おわり

pushすればこんな感じで動く ( Actionsタブから確認)

image.png

  • 上の画像ではdevelopブランチがpushされたので、ステージング環境のdiffとdeployが行われている
    • 対象外のstepはスキップされるらしい
    • 途中で失敗すると、後のstepはスキップされる
  • 画像右上の Artifactsはフロー内でアップロードしたfrontendのビルド成果物
  • 画像右上の Re-run all jobsでActionsを回し直せる
    • 偶然失敗したケースならpushし直さなくて済む
    • CircleCIと比べると、rerun対象のjobを選べない (全jobやり直してしまう)

かなり多くのサイトを参考にしたけど、あまりにもそれぞれのサイトから断片的な情報を組み合わせたので記憶の彼方に。。。 :innocent:

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?