見出し画像

AWS CDKで本番環境と開発環境を実行時に指定してデプロイできるようにしたこと

この春入社しました、メディア事業本部サービス開発部の島貫です。
(本稿は朝日新聞社の2024年Qiitaアドベントカレンダーの投稿です)

今年8月にチームに配属されてから、チーム内で保守・運用しているwebアプリケーションサービスの、検証環境構築作業で行ったことを書いてみます。


背景・目的

該当のwebアプリケーションはAWSを使用して構成されており、その中でユーザーが入力したテキストが事前に用意している数十パターンのテキストのどれに近いかを判定して値として返すAPIがあります。検証系ではそれが作成できておらず、この部分は本番系のAPIにつながっていました。今後改修などがあった場合に今のままではもろもろ不便ということで、AWS CDKを用いて検証系のものをデプロイすることが目標でした。

また検証系と本番系は同じAWSアカウント上にあるので、検証系のデプロイが本番系に影響しないよう、コードは両者で完全に同一なまま検証系か本番系かを明示的に指定してデプロイできるようにしたいと考えていました。

AWSについては新人研修で触れたのみだったので、初歩の初歩から書いていきたいと思います。

なお、実際に稼働しているサービスであるためコードの一部など伏せています。

事前準備①

まずはaws cliをインストールしました。

brew install awscli

■インストール完了の確認

aws --version 
# 出力
aws-cli/2.17.45 Python/3.11.9 Darwin/23.3.0 source/arm64

続いて初期設定をします。ここでは以下の記事などを参考にしました。

まず、アクセスするAWSのIAMの画面から自分のアカウントのアクセスキーを作成しました。これを使って以下のような~/.aws/configureファイルと~/.aws/credentialsファイルを作成します。

■~/.aws/config

cat ~/.aws/config
# 出力
[default]                                                                 
region = ap-northeast-1
output = json

■~/.aws/credentials

cat ~/.aws/credentials
# 出力
[default]
aws_access_key_id = *****
aws_secret_access_key = *****

コマンドラインで作成する場合はaws configureで作成します。

また、MFAを設定しているので、以下の記事などを参考に作業時には毎度アクセスキーを発行して、~/.aws/credentialsを編集しています。

■アクセスキーの発行

aws sts get-session-token --serial-number {MFAに割り当てられたIAM} --token-code XXXXXX

実行すると以下のような出力が返ってきます。

{
    "Credentials": {
        "AccessKeyId": "XXXXXXXXXXXXXXXX",
        "SecretAccessKey": "YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY",
        "SessionToken": "**********************************************",
        "Expiration": "2024-11-20T22:00:39Z"
    }
}

上で得られたキーを使って、~/.aws/credentialsを以下のように書き換えます。

cat ~/.aws/credentials
# 出力
[default]
aws_access_key_id = XXXXXXXXXXXXXXXX
aws_secret_access_key = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
aws_session_token = **********************************************

上記の準備をして以下のコマンドなどでレスポンスがあればAWSにアクセスできています。

aws s3 ls

次にaws cdkをインストールしました。

npm install -g aws-cdk

以上が事前準備①です。

事前準備②

AWSでチュートリアルが用意されているのを教えてもらい、まずはこれで自分の環境から該当AWSアカウントにcdk deployができるか試しました。

詳細は省略させていただきますが、必要な権限周りのこともエラーを見ながら知ることができて、勉強になりました。

実際にコードを書き換えてみる

まず、使用環境は以下の通りです。

aws --version
# 出力
aws-cli/2.17.45 Python/3.11.9 Darwin/23.3.0 source/arm64

node --version
# 出力
v18.20.4

cdk --version
# 出力
2.156.0 (build 2966832)

検証系と本番系でコードは完全に同一のままで、Lambda、API、Stack名はそれぞれ変えたいので、以下の記事など参考にデプロイ実行時のコマンドラインから引数で渡せるようにする方向性で実装しました。

まずapp.pyを以下のようにして、引数から変数を取得するようにします。

#!/usr/bin/env python3

import aws_cdk as cdk
import importlib 
from AAA_stack import XxxxxStack
  
app = cdk.App()

# パラメータを取得
param_env = app.node.try_get_context("ENV")

XxxxxStack(app, "XxxxxStack" + param_env, param_env=param_env)

app.synth()  

次に、_stack.pyを以下のようにして受け取った変数がLambdaやAPIの名前に入るようにします。

### ~略~ ###

class XxxxxStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, param_env, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        lambda_handler = _lambda.DockerImageFunction(
            self,
            "xxxxx-lambda" + param_env,
            function_name="xxxxx-lambda" + param_env,
            code=_lambda.DockerImageCode.from_image_asset(
                directory="docker"
                ),
            ### 〜略〜 ###
        )

        api = apigw.LambdaRestApi(
            self,
            "xxxxx-api-gw" + param_env,
            ### 〜略〜 ###
        ),
            
### ~略~ ###

これでデプロイ実行時に以下のように引数を渡して実際にデプロイしてみると、無事検証系と本番系でそれぞれ指定したデプロイを行うことができました。

■デプロイ時のコマンド

# 検証系
cdk deploy -c ENV=-dev

# 本番系 
cdk deploy -c ENV=-prod

■AWSの画面

  • Cloud Formation

  • Lambda

  • API Gateway

それぞれデプロイ時に渡した「-dev」と「-prod」が名前にくっついて作成されており、検証系と本番系が干渉せずに別々で作成できていそうです。

デプロイしたAPIの動作確認

続いてAPIの動作確認を行いました。

APIの動作確認にはgoogleの拡張機能Talend API Testerを教えてもらい、使用しています。

METHODを設定後APIのURLを入れて、ユーザー側で入力してPOSTするテキストを入れて、APIを叩いてみます。

その結果・・・

502エラーとなりました。応答していません。

ただdockerを使ってローカルで以下のようなテストを行うと出力が返ってきました。おそらくAPIはできていそうで、何か動かない原因がありそうでした。

docker build -t my-lambda-image .
docker run -p 9000:8080 my-lambda-image
curl -X POST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{
  "body": "{\"text\": \"あああああ\"}"
}'

# 出力
03 Oct 2024 08:29:17,938 [INFO] (rapid) INIT RTDONE(status: success)
03 Oct 2024 08:29:17,938 [INFO] (rapid) INIT REPORT(durationMs: 5104.397000)
03 Oct 2024 08:29:17,938 [INFO] (rapid) INVOKE START(requestId: 1b3561c5-ecaa-4d50-8b44-fa98d0c7debf){"text": "あああああ", "XXXXX": "yy"}
03 Oct 2024 08:29:17,967 [INFO] (rapid) INVOKE RTDONE(status: success, produced bytes: 0, duration: 29.144000ms)
END RequestId: 1b3561c5-ecaa-4d50-8b44-fa98d0c7debf
REPORT RequestId: 1b3561c5-ecaa-4d50-8b44-fa98d0c7debf  Init Duration: 1.24 ms  Duration: 5133.88 ms  Billed Duration: 5134 ms  Memory Size: 3008 MB  Max Memory Used: 3008 MB 

そこで、502エラー出力時にCloudWatchに出ている「Runtime.InvalidEntrypoint」というエラーログを調べていると以下のような記事を見つけました。

Lambdaのアーキテクチャによっては上記のエラーが出るようです。

そこで、502エラーが出ているLambdaのアーキテクチャをAWSコンソール上でみると、

(dockerのコマンドでも確認できます)

docker inspect --format='{{.Architecture}}' my-lambda-image

上の通りLambdaのアーキテクチャについて何も指定していない場合、デフォルトだと「x86_64」になっていました。そこで、これをarm64にした場合に動くか試してみることにしました。

アーキテクチャの指定を入れてみたら動いた

以下のように_stack.py内にアーキテクチャの指定を入れてみます。

        lambda_handler = _lambda.DockerImageFunction(
            self,
            "xxxxx-lambda" + param_env,
            function_name="xxxxx-lambda" + param_env,
            
            architecture=_lambda.Architecture.ARM_64, # Architectureを指定
             ### 〜略〜 ###
        )

上のように_stack.pyを書き換えてデプロイするとLambdaのアーキテクチャがarm64になっています。

この状態でもう一度APIの動作確認をしてみます。

無事レスポンスが返ってくるようになりました。

まだ理解が浅いですが参考記事の通り、自分の使用環境とLambda関数のアーキテクチャの対応が合っていないとLambda関数実行時にエラーとなるようです。ちなみに自分が作業した際の使用環境はM3搭載のMacBookAirです。

以上つらつらと行なったことを書いてきましたが、本番系と検証系それぞれのLambda+APIのデプロイを実行時の引数を変えるだけで実行できるようになり、正常動作もできていそうです。今後webアプリケーションの改修などを行なった場合、この方法で本番への影響なく検証系でのテストをしていきたいと思います。

また引き続きちょこちょこ手を動かしつつ、落とし穴にもハマりつつ、AWSを用いたwebアプリケーションの構築について知識を習得していけたらと思います。