4
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 1 year has passed since last update.

AWS CDK v2 + L1 + TypeScriptで苦しんで構築するApp Runner

Posted at

AWS CDK v2で若干苦しんだので書きます。

環境

AppRunnerにRailsアプリをデプロイしようと思います。
たぶん無料枠超えてると思うので試す時はご注意を。

CDKの環境を作る

いつも通りcdk initで作ります。
CDK v2.54.0なので、CDK v2のテンプレートで作成されます。

image.png

VPC書く

よしなに書くだけですね。

const vpc = new ec2.Vpc(this, 'VPC', {
  subnetConfiguration: [
    {
      cidrMask: 24,
      name: 'rds',
      subnetType: ec2.SubnetType.PRIVATE_ISOLATED
    }
  ]
})

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

App Runnerのインスタンス用とRDS用のセキュリティグループを作成して、RDSへのアクセスはApp Runnerのセキュリティグループからのみ許可するようにします。

const appRunnerSecurityGroup = new ec2.SecurityGroup(this, 'AppRunnerSecurityGroup', { vpc })
const rdsSecurityGroup = new ec2.SecurityGroup(this, 'RDSSecurityGroup', { vpc })
rdsSecurityGroup.addIngressRule(appRunnerSecurityGroup, ec2.Port.tcp(3306))

RDSインスタンス書く

こちらも同じくよしなに書けばいい感じに作ってくれます。
ただ、デフォルトでm5.largeだった気がするのでinstanceTypeは必ず指定しておいたほうが良いかと。

今回は使い捨てなので平文パスワードですが実際はSSMやSecrets Managerの値を使ってくださいね!

const dbInstance = new rds.DatabaseInstance(this, 'RDS', {
  engine: rds.DatabaseInstanceEngine.MYSQL,
  vpc: vpc,
  vpcSubnets: {
    subnets: vpc.isolatedSubnets
  },
  securityGroups: [rdsSecurityGroup],
  instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
  credentials: {
    username: 'root',
    password: SecretValue.unsafePlainText('INSECURE_PASSWORD')
  }
})

ECR作る

今回は作って消すのでremovalPolicyを設定してcdk destroyで消せるようにしておきます。

const repository = new ecr.Repository(this, 'ECR', {
  removalPolicy: RemovalPolicy.DESTROY
})

AppRunner書く

本題です。
先程まではL2コンストラクト使ってたんですが、ここからはL1しか無いのでCloudFormationのドキュメントとにらめっこしながら書く必要があります。

aws-cdk-lib/aws_apprunnerのドキュメントにはほとんど情報がないのでとにかくドキュメントを読むしか無いです。
CDKの中身はCFnなのでそうなるよなーって感じですね。

App Runner用ロールを作る

インスタンスロールとECRアクセス用のロールを作成します。
ECRのロールにAWSAppRunnerServicePolicyForECRAccessを使ってますがどのリポジトリにもアクセスできてしまうため制限したい場合は自分でポリシー作成したほうが良いと思います。

const instanceRole = new iam.Role(this, 'AppRunnerInstanceRole', {
  assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com')
})
const ecrAccessRole = new iam.Role(this, 'AppRunnerECRAccessRole', {
  assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'),
  managedPolicies: [
    iam.ManagedPolicy.fromAwsManagedPolicyName(
      'service-role/AWSAppRunnerServicePolicyForECRAccess',
    ),
  ]
})

VPC connector書く

RDSがプライベートなサブネットにいるので、App Runnerのインスタンスが通信できるようVPCコネクターを作成する必要があります。
L1なのでISubnet[]じゃなくてsubnetIdの方を渡してあげないといけないです。

const vpcConnector = new apprunner.CfnVpcConnector(this, 'VPCConnector', {
  subnets: vpc.isolatedSubnets.map((subnet) => subnet.subnetId),
  securityGroups: [appRunnerSecurityGroup.securityGroupId]
})

App Runnerのインスタンスヘルスチェックに失敗すると全部ロールバックされるので、この時点で一旦デプロイしてしまったほうが楽かもしれないです。

App Runnerサービス書く

サービスを書いていきます。
healthCheckConfigurationにハマりポイントがあって、プロトコルをHTTPにしないとpathプロパティを設定しても無視されます。
自分はこれで3時間位潰しました。

ドキュメントにバッチリ書いてありますね。

Path is only applicable when you set Protocol to HTTP.

また、サービスがヘルスチェックに通らないとCloudFormationのデプロイに失敗するので予めECRにヘルスチェックが通るイメージを上げておく必要があります。
今回は初期化して適当に設定合わせたRailsプロジェクトを上げてます。

データベースパスワードなどのシークレットを平文で渡してますが、App Runnerがシークレットな環境変数をサポートしていないのでとりあえずやってます。
本来はRailsアプリ側でSSMやSecrets Managerからシークレットを取得するようにしないといけないです。

new apprunner.CfnService(this, 'AppRunnerService', {
  healthCheckConfiguration: {
    protocol: 'HTTP',
    path: '/',
  },
  networkConfiguration: {
    egressConfiguration: {
      egressType: 'VPC',
      vpcConnectorArn: vpcConnector.attrVpcConnectorArn,
    },
  },
  sourceConfiguration: {
    authenticationConfiguration: {
      accessRoleArn: ecrAccessRole.roleArn,
    },
    imageRepository: {
      imageRepositoryType: 'ECR',
      imageIdentifier: repository.repositoryUriForTag('latest'),
      imageConfiguration: {
        port: '3000',
        runtimeEnvironmentVariables: [
          {
            name: 'RAILS_ENV',
            value: 'production'
          },
          {
            name: 'RAILS_LOG_TO_STDOUT',
            value: 'true',
          },
          {
            name: 'DATABASE_HOST',
            value: dbInstance.dbInstanceEndpointAddress,
          },
          {
            name: 'DATABASE_PASSWORD',
            value: 'INSECURE_PASSWORD'
          }
        ]
      }
    },
  },
  instanceConfiguration: {
    cpu: '1024',
    memory: '2048',
    instanceRoleArn: instanceRole.roleArn,
  },
})

デプロイ

最終的に以下の形になりました。

import { 
  Stack,
  StackProps,
  aws_apprunner as apprunner, 
  aws_ec2 as ec2, 
  aws_rds as rds,
  aws_iam as iam,
  aws_ecr as ecr,
  RemovalPolicy,
  SecretValue,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class MyAppStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    /*
     * VPC
     */
    const vpc = new ec2.Vpc(this, 'VPC', {
      natGateways: 0,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'rds',
          subnetType: ec2.SubnetType.PRIVATE_ISOLATED
        }
      ]
    })

    /*
     * Security groups
     */
    const appRunnerSecurityGroup = new ec2.SecurityGroup(this, 'AppRunnerSecurityGroup', { vpc })
    const rdsSecurityGroup = new ec2.SecurityGroup(this, 'RDSSecurityGroup', { vpc })
    rdsSecurityGroup.addIngressRule(appRunnerSecurityGroup, ec2.Port.tcp(3306))

    /*
     * RDS
     */
    const dbInstance = new rds.DatabaseInstance(this, 'RDS', {
      engine: rds.DatabaseInstanceEngine.MYSQL,
      vpc: vpc,
      vpcSubnets: {
        subnets: vpc.isolatedSubnets
      },
      securityGroups: [rdsSecurityGroup],
      instanceType: ec2.InstanceType.of(ec2.InstanceClass.T2, ec2.InstanceSize.MICRO),
      credentials: {
        username: 'root',
        password: SecretValue.unsafePlainText('INSECURE_PASSWORD')
      }
    })

    /*
     * IAM
     */
    const instanceRole = new iam.Role(this, 'AppRunnerInstanceRole', {
      assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com')
    })
    const ecrAccessRole = new iam.Role(this, 'AppRunnerECRAccessRole', {
      assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'service-role/AWSAppRunnerServicePolicyForECRAccess',
        ),
      ]
    })

    /*
     * ECR
     */
    const repository = new ecr.Repository(this, 'ECR', {
      removalPolicy: RemovalPolicy.DESTROY
    })

    /*
     * App Runner
     */
    const vpcConnector = new apprunner.CfnVpcConnector(this, 'VPCConnector', {
      subnets: vpc.isolatedSubnets.map((subnet) => subnet.subnetId),
      securityGroups: [appRunnerSecurityGroup.securityGroupId]
    })

    new apprunner.CfnService(this, 'AppRunnerService', {
      healthCheckConfiguration: {
        protocol: 'HTTP',
        path: '/',
      },
      networkConfiguration: {
        egressConfiguration: {
          egressType: 'VPC',
          vpcConnectorArn: vpcConnector.attrVpcConnectorArn,
        },
      },
      sourceConfiguration: {
        authenticationConfiguration: {
          accessRoleArn: ecrAccessRole.roleArn,
        },
        imageRepository: {
          imageRepositoryType: 'ECR',
          imageIdentifier: repository.repositoryUriForTag('latest'),
          imageConfiguration: {
            port: '3000',
            runtimeEnvironmentVariables: [
              {
                name: 'RAILS_ENV',
                value: 'production'
              },
              {
                name: 'RAILS_LOG_TO_STDOUT',
                value: 'true',
              },
              {
                name: 'DATABASE_HOST',
                value: dbInstance.dbInstanceEndpointAddress,
              },
              {
                name: 'DATABASE_PASSWORD',
                value: 'INSECURE_PASSWORD'
              }
            ]
          }
        },
      },
      instanceConfiguration: {
        cpu: '1024',
        memory: '2048',
        instanceRoleArn: instanceRole.roleArn,
      },
    })
  }
}

起動時のスクリプトはこんな感じで用意しました。
必要に応じてアセットビルドしたりforeman等を呼び出したりすると良いと思います。

#!/bin/bash
set -eu

bundle exec rails db:create
bundle exec rails db:migrate
bundle exec rails server -p 3000 -b '0.0.0.0'

作成したECRにDockerイメージをpushした状態でcdk deployしたら無事デプロイできると思います。

動作確認

App Runnerサービスに表示されているエンドポイントURLにアクセスしたら無事Railsアプリが表示されると思います。
image.png

データベース作成&マイグレーションもちゃんと行われていますね。
image.png

お掃除

一通り試したのでcdk destroyで全部まるっと消してしまいましょう。

ECRはイメージがあると消えないことがあるので、cdk destroyでエラーが出たら手動で削除してください。
removalPolicy設定してるのになんで消えないんですかね?

おわり

1日遅れてしまいましたが10日目でした。

今回はECRでデプロイしましたが、GitHubからCDKでデプロイする方法は未だに思いついてないです。
API叩けば行けると思うんですが...

今のApp Runnerでは機密情報を環境変数に設定するといったことができないので早くできるようになってほしいなといった感じです。

4
1
1

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
4
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?