この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、CX事業本部 IoT事業部の若槻です。
最近、AWS CDK v2やGitHub Actionsについて調査することが多いので、今回はそれらの合わせ技として、AWS CDK v2 + GitHub ActionsでReactアプリをデプロイしてみました。
環境
$ cdk --version
2.20.0 (build 738ef49)
$ npm --version
8.5.0
$ node --version
v14.17.0
やってみた
GitHub Repositoryの作成
GitHub上に適当な名前(今回はaws-cdk-v2-project
)Repositoryを作成したら、ローカルにCloneします。
$ git clone https://github.com/<Account>/aws-cdk-v2-project.git aws-cdk-v2-project
$ cd aws-cdk-v2-project
AWS CDK v2プロジェクトの作成
cdk init
を使用して新しいAWS CDK v2プロジェクトを作成します。
$ npx cdk init --language typescript
一部の型パッケージは手動でインストールします。
$ npm i --save-dev @types/source-map-support
AWS CDK v2(今回は2.19.0
)のプロジェクトが作成できました。
$ npm ls --depth=0
aws-cdk-v2-project@0.1.0 /Users/wakatsuki.ryuta/projects/cm-rwakatsuki/aws-cdk-v2-project
├── @types/jest@26.0.24
├── @types/node@10.17.27
├── @types/source-map-support@0.5.4
├── aws-cdk-lib@2.20.0
├── aws-cdk@2.20.0
├── constructs@10.0.109
├── jest@26.6.3
├── source-map-support@0.5.21
├── ts-jest@26.5.6
├── ts-node@9.1.1
└── typescript@3.9.10
CDK Bootstrapを行います。
$ cdk bootstrap --region us-east-1
us-east-1
リージョンでBootstrapを行うのは、今回は使用しませんが、CloudFront Distributionに設定するACM Certificateはus-east-1
にしか作成できないという制限があるためです。
.gitignore
に、次節で作成するReactアプリのパスを除外する記述を追加します。これにより*.d.ts
ファイルなどを不本意にGit管理から除外されないようにします。
.gitignore
!jest.config.js
*.d.ts
node_modules
# CDK asset staging directory
.cdk.staging
cdk.out
!/web/**
Reactアプリの作成
create-react-appを使用して、サブディレクトリ配下にReactアプリを作成します。
$ npx create-react-app web --template typescript
作成できたら、動作確認のためローカルでアプリを起動してみます。
$ npm --prefix web start
起動できました。
AWS CDKでデプロイ用のコードを書く
CDK Appの定義でデプロイ先のリージョンをus-east-1
に指定します。
bin/aws-cdk-v2-project.ts
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { AwsCdkV2ProjectStack } from '../lib/aws-cdk-v2-project-stack';
const app = new cdk.App();
new AwsCdkV2ProjectStack(app, 'AwsCdkV2ProjectStack', {
env: { region: 'us-east-1' },
});
Reactアプリをデプロイするためのコードを書きます。
lib/aws-cdk-v2-project-stack.ts
import { Construct } from 'constructs';
import {
Stack,
StackProps,
aws_s3,
aws_cloudfront,
aws_cloudfront_origins,
aws_s3_deployment,
aws_iam,
RemovalPolicy,
Duration,
} from 'aws-cdk-lib';
export class AwsCdkV2ProjectStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);
const websiteBucket = new aws_s3.Bucket(this, 'WebsiteBucket', {
removalPolicy: RemovalPolicy.DESTROY,
});
const originAccessIdentity = new aws_cloudfront.OriginAccessIdentity(
this,
'OriginAccessIdentity',
{
comment: 'website-distribution-originAccessIdentity',
}
);
const webSiteBucketPolicyStatement = new aws_iam.PolicyStatement({
actions: ['s3:GetObject'],
effect: aws_iam.Effect.ALLOW,
principals: [
new aws_iam.CanonicalUserPrincipal(
originAccessIdentity.cloudFrontOriginAccessIdentityS3CanonicalUserId
),
],
resources: [`${websiteBucket.bucketArn}/*`],
});
websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement);
const distribution = new aws_cloudfront.Distribution(this, 'distribution', {
comment: 'website-distribution',
defaultRootObject: 'index.html',
errorResponses: [
{
ttl: Duration.seconds(300),
httpStatus: 403,
responseHttpStatus: 403,
responsePagePath: '/index.html',
},
{
ttl: Duration.seconds(300),
httpStatus: 404,
responseHttpStatus: 404,
responsePagePath: '/index.html',
},
],
defaultBehavior: {
allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
cachedMethods: aws_cloudfront.CachedMethods.CACHE_GET_HEAD,
cachePolicy: aws_cloudfront.CachePolicy.CACHING_OPTIMIZED,
viewerProtocolPolicy:
aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
origin: new aws_cloudfront_origins.S3Origin(websiteBucket, {
originAccessIdentity,
}),
},
priceClass: aws_cloudfront.PriceClass.PRICE_CLASS_ALL,
});
new aws_s3_deployment.BucketDeployment(this, 'WebsiteDeploy', {
sources: [aws_s3_deployment.Source.asset('./web/build')],
destinationBucket: websiteBucket,
distribution: distribution,
distributionPaths: ['/*'],
});
}
}
package.json
にデプロイ用のscriptsを追記します。
package.json
{
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk",
"deploy": "cdk deploy --require-approval never"
}
}
動作確認のためローカルからデプロイしてみます。
まずReactアプリをビルドします。
$ npm --prefix web run build
ReactアプリをCDKでデプロイします。
$ npm run deploy
CloudFrontのDistribution Domainをブラウザで開くと、デプロイされたReactアプリにアクセスできました。
GitHubとAWSのOIDC設定
GitHub ActionsではOpen ID Connectがサポートされたので使わない手はないでしょう。
GitHubとAWSのOIDC連携で必要な設定を行います。
ID Providerの作成
GitHubのID Providerが未作成の場合は、Create Identity Providerで次のように指定して、ID Providerを作成します。
- Provider type:
OpenID Connect
- Provider URL:
https://token.actions.githubusercontent.com
- Audience:
sts.amazonaws.com
IAM Roleの作成
GitHub Actions Workflowで使用するIAM Roleを作成します。
作成したIAM Roleに、CDKによるデプロイが行えるポリシーをアタッチします。必要な権限はCDKでどのリソースを作成するかによりけりですが、必要に応じて絞ってください。今回はすべてのリソースの作成に対応できるように*
を指定しています。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "*",
"Resource": "*"
}
]
}
信頼ポリシーを次のように編集して、特定のGitHub Repositoryのブランチでのアクション時にのみ、ID Providerを信頼するようにします。Federated
には先程作成したID ProviderのArnを指定します。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "<ID Provider Arn>"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:<GitHub Account>/<GitHub Repository>:ref:refs/heads/*"
}
}
}
]
}
作成したIRM RoleのArnを控えます。
GitHub Actionsの設定
Repositoryの[Settings]-[Security]-[Secrets]-[Actions]で[New repository secret]をクリック。
適当な名前(今回はAWS_OIDC_ROLE_ARN
)で先程控えたIRM RoleのArnを値に指定したSecretを作成します。
GitHub ActionsのWorkflowファイルを作成します。
$ mkdir .github
$ mkdir .github/workflows
$ touch .github/workflows/cd.yaml
Workflowファイルを次のように編集します。
.github/workflows/cd.yaml
on:
push:
paths-ignore:
- '**/*.md'
permissions:
id-token: write
contents: read
env:
AWS_OIDC_ROLE_ARN: ${{ secrets.AWS_OIDC_ROLE_ARN }}
AWS_REGION: us-east-1
jobs:
aws-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Assume Role
uses: aws-actions/configure-aws-credentials@v1
with:
role-to-assume: ${{ env.AWS_OIDC_ROLE_ARN }}
aws-region: ${{env.AWS_REGION}}
- name: Cache CDK Dependency
uses: actions/cache@v3
id: cache_cdk_dependency_id
env:
cache-name: cache-cdk-dependency
with:
path: node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}
restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-
- name: Install CDK Dependency
if: ${{ steps.cache_cdk_dependency_id.outputs.cache-hit != 'true' }}
run: npm install
- name: Cache Web Dependency
uses: actions/cache@v3
id: cache_web_dependency_id
env:
cache-name: cache-web-dependency
with:
path: web/node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/package-lock.json') }}
restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-
- name: Install Web Dependency
if: ${{ steps.cache_web_dependency_id.outputs.cache-hit != 'true' }}
run: npm --prefix web install
- name: Cache Build Web
uses: actions/cache@v3
id: cache_build_web_id
env:
cache-name: cache-build-web
with:
path: web/build
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('web/*', '!web/node_modules', '!web/**/*.md', '!web/build') }}
- name: Build Web
if: ${{ steps.cache_build_web_id.outputs.cache-hit != 'true' }}
run: npm --prefix web run build
- name: Deploy
run: npm run deploy
actions/cache
のあたりが凝っていそうな感じですが、**/node_modules
やweb/build
のキャッシュを頑張っています。これに関してはまたの機会に詳しく紹介したいと思います。
デプロイしてみる
Remote RepositoryにGit Pushを行います。
するとGutHub ActionsでWorkflowが実行されてCDK Deployが行われ、正常に完了しました!
Reactアプリの実装を修正(文言変更)します。
web/src/App.tsx
<a
className='App-link'
href='https://reactjs.org'
target='_blank'
rel='noopener noreferrer'
>
Hello React
</a>
Git PushするとWorkflowが実行されました。
アプリにアクセスすると修正が反映されています!
おわりに
AWS CDK v2 + GitHub ActionsでReactアプリをデプロイしてみました。
個別の技術要素についてはよく触っていたので何てことないだろうと思いきや、合わせて扱うことによって個別の時には想定していなかったハマり方などをしたりしました(凡ミスが多かったですが)。そういう点含めて一連の手順を確認できてよかったです。
参考
- AWS CDKでReactアプリをデプロイしてみた | DevelopersIO
- AWS CDK V2でCloudFront+S3の静的サイトホスティングを構築してみた | DevelopersIO
- GitHub ActionsでOpenID Connectを使用してAWS CDKのデプロイを実行してみた | DevelopersIO
- GitHub Actionsでactions/setup-nodeだけでnode_modulesをキャッシュできるのか試してみた | DevelopersIO
- node.js - Could not find a required file. Name: index.html - Stack Overflow
以上