kakakakakku blog

Weekly Tech Blog: Keep on Learning!

AWS Lambda 関数 (Python) の import 時間を計測しよう / -X importtime オプション or 環境変数 PYTHONPROFILEIMPORTTIME

AWS Lambda の初期化フェーズ (INIT) は「10秒」に制限されている.例えば,AWS Lambda 関数の「ベストプラクティス」を意識してハンドラ外に実装した処理が長くなったり,AWS Lambda 関数 (Python) で機械学習系のライブラリなどを多く import しようとして遅くなり,結果的に INIT のタイムアウトが出てしまうことがあったりする.

INIT_REPORT Init Duration: 10020.95 ms Phase: init Status: timeout

AWS Lambda 関数 (Python) で import の最適化をするためには「そもそもどの import が遅いのか」を確認したく,import の前後に計測用のログを仕込むこともできるけど,Python 3.7 から使える -X importtime オプション(もしくは環境変数 PYTHONPROFILEIMPORTTIME)が便利で AWS Lambda 関数でも使えるので紹介したいと思う💡

docs.python.org

今回は以下の「計4パターン」で試していく❗️

  1. AWS Lambda 関数 (ZIP) x PYTHONPROFILEIMPORTTIME 環境変数
  2. AWS Lambda 関数 (ZIP) x Layer x -X importtime オプション
  3. AWS Lambda 関数 (Image) x PYTHONPROFILEIMPORTTIME 環境変数
  4. AWS Lambda 関数 (Image) x -X importtime オプション

サンプルコード

あくまでサンプルなので意味はないけど,今回は boto3 / json / numpy / requests を import するコードを準備した👌

👾 requirements.txt

boto3
numpy
requests

👾 app.py

import boto3
import json
import numpy as np
import requests


def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': json.dumps({'message': 'hello world'})
    }

1. AWS Lambda 関数 (ZIP) x PYTHONPROFILEIMPORTTIME 環境変数

👾 template.yaml

AWS SAM の template.yaml を以下のように書いた.

Environment.VariablesPYTHONPROFILEIMPORTTIME: 1 を設定している😃

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31

Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: importtime-env
      CodeUri: src/
      Handler: app.lambda_handler
      Runtime: python3.12
      Architectures:
        - x86_64
      Environment:
        Variables:
          PYTHONPROFILEIMPORTTIME: 1

AWS Lambda 関数 importtime-env をデプロイして実行すると以下のように import 時間がログに出力されていた❗️(ちなみに AWS CloudWatch Logs に出力されたログは表形式が崩れてしまっていて見にくかったため,以下のログは AWS SAM の sam local invoke コマンドの結果を載せている🙏)

import time: self [us] | cumulative | imported package
import time:      1041 |       1041 |   _io
import time:        89 |         89 |   marshal
import time:       826 |        826 |   posix
import time:      2987 |       4941 | _frozen_importlib_external
import time:       361 |        361 |   time
import time:       446 |        807 | zipimport
import time:        33 |         33 |     _codecs
import time:       424 |        456 |   codecs
import time:       542 |        542 |   encodings.aliases
import time:      3553 |       4551 | encodings
import time:       170 |        170 | encodings.utf_8
import time:       158 |        158 | _signal
import time:        44 |         44 |     _abc
import time:       427 |        471 |   abc
import time:       857 |       1327 | io
import time:        82 |         82 |       _stat
import time:       100 |        181 |     stat
import time:      1870 |       1870 |     _collections_abc
import time:        38 |         38 |       genericpath
import time:        82 |        120 |     posixpath
import time:       992 |       3162 |   os
import time:        76 |         76 |   _sitebuiltins
import time:       186 |        186 |   sitecustomize
import time:        53 |         53 |   usercustomize
import time:      1784 |       5259 | site
import time:       391 |        391 |         types
import time:        65 |         65 |           _operator
import time:       492 |        557 |         operator
import time:       226 |        226 |             itertools
import time:       142 |        142 |             keyword
import time:       217 |        217 |             reprlib
import time:       105 |        105 |             _collections
import time:       739 |       1426 |           collections
import time:        87 |         87 |           _functools
import time:      1574 |       3087 |         functools
import time:      1797 |       5829 |       enum
import time:        70 |         70 |         _sre
import time:       288 |        288 |           re._constants
import time:       349 |        636 |         re._parser
import time:       215 |        215 |         re._casefix
import time:       373 |       1294 |       re._compiler
import time:       165 |        165 |       copyreg
import time:      1651 |       8938 |     re
import time:       906 |        906 |       _json
import time:      1298 |       2203 |     json.scanner
import time:      1010 |      12151 |   json.decoder
import time:      1093 |       1093 |   json.encoder
import time:       452 |      13694 | json
import time:       142 |        142 |     collections.abc
import time:       152 |        152 |         token
import time:        56 |         56 |         _tokenize
import time:      1129 |       1337 |       tokenize
import time:       151 |       1487 |     linecache
import time:       890 |        890 |     textwrap
import time:       464 |        464 |     contextlib
import time:       671 |       3653 |   traceback
import time:       266 |        266 |   warnings
import time:       621 |        621 |     _weakrefset
import time:       479 |       1100 |   weakref
import time:        40 |         40 |     _string
import time:       569 |        609 |   string
import time:       580 |        580 |   threading
import time:        41 |         41 |   atexit
import time:      2290 |       8536 | logging
import time:       174 |        174 |   awslambdaric
import time:       309 |        309 |     importlib
import time:       188 |        188 |     awslambdaric.lambda_context
import time:        97 |         97 |       awslambdaric.lambda_runtime_exception
import time:      1399 |       1399 |       runtime_client
import time:       289 |        289 |             numbers
import time:       979 |       1267 |           _decimal
import time:       134 |       1401 |         decimal
import time:       263 |        263 |         math
import time:       131 |        131 |           __future__
import time:       211 |        211 |           simplejson.errors
import time:       123 |        123 |           simplejson.raw_json
import time:       192 |        192 |               _struct
import time:       111 |        303 |             struct
import time:       128 |        128 |             simplejson.compat
import time:       206 |        206 |               simplejson._speedups
import time:       156 |        362 |             simplejson.scanner
import time:       247 |       1040 |           simplejson.decoder
import time:       542 |        542 |           simplejson.encoder
import time:       368 |       2412 |         simplejson
import time:       232 |       4305 |       awslambdaric.lambda_runtime_marshaller
import time:       553 |       6353 |     awslambdaric.lambda_runtime_client
import time:       510 |        510 |     awslambdaric.lambda_runtime_log_utils
import time:       330 |       7689 |   awslambdaric.bootstrap
import time:       166 |       8028 | awslambdaric.__main__
import time:       684 |        684 |   concurrent
import time:       653 |        653 |   concurrent.futures._base
import time:       233 |       1569 | concurrent.futures
import time:       150 |        150 |       _heapq
import time:       178 |        327 |     heapq
import time:       215 |        215 |     _queue
import time:       306 |        847 |   queue
import time:       298 |       1145 | concurrent.futures.thread
import time:       225 |        225 |     errno
import time:       715 |        715 |       _socket
import time:       199 |        199 |         select
import time:       616 |        815 |       selectors
import time:       320 |        320 |       array
import time:      1888 |       3736 |     socket
import time:      2242 |       2242 |         botocore
import time:      1241 |       1241 |         botocore.vendored
import time:      2355 |       2355 |                     botocore.vendored.requests.packages.urllib3.exceptions
import time:      1555 |       3910 |                   botocore.vendored.requests.packages.urllib3
import time:      1041 |       4950 |                 botocore.vendored.requests.packages
import time:        18 |       4968 |               botocore.vendored.requests.packages.urllib3
import time:        24 |       4991 |             botocore.vendored.requests.packages.urllib3.exceptions
import time:      1767 |       6758 |           botocore.vendored.requests.exceptions
import time:      1630 |       8387 |         botocore.vendored.requests
import time:      8803 |      20672 |       botocore.exceptions
import time:      2310 |      22981 |     boto3.exceptions
import time:      4605 |      31546 |   boto3.compat
import time:       208 |        208 |     copy
import time:        86 |         86 |         _wmi
import time:       491 |        576 |       platform
import time:       193 |        193 |                   _bisect
import time:       132 |        325 |                 bisect
import time:       189 |        189 |                 _random
import time:        82 |         82 |                 _sha512
import time:      7551 |       7551 |                   _hashlib
import time:       211 |        211 |                   _blake2
import time:      1203 |       8965 |                 hashlib
import time:       932 |      10490 |               random
import time:       190 |        190 |                         _ast
import time:      1253 |       1442 |                       ast
import time:       160 |        160 |                           _opcode
import time:       548 |        708 |                         opcode
import time:       735 |       1443 |                       dis
import time:        68 |         68 |                       importlib.machinery
import time:      2190 |       5141 |                     inspect
import time:       751 |       5892 |                   jmespath.compat
import time:      1454 |       7346 |                 jmespath.exceptions
import time:      1945 |       9291 |               jmespath.lexer
import time:      1121 |       1121 |               jmespath.ast
import time:      2542 |       2542 |                 jmespath.functions
import time:      2458 |       5000 |               jmespath.visitor
import time:      5000 |      30900 |             jmespath.parser
import time:      1373 |      32273 |           jmespath
import time:       894 |        894 |                   botocore.docs.bcdoc
import time:       570 |        570 |                       _datetime
import time:       161 |        730 |                     datetime
import time:       694 |        694 |                       http
import time:       158 |        158 |                         email
import time:       484 |        484 |                           email.errors
import time:       218 |        218 |                               binascii
import time:       291 |        291 |                               email.quoprimime
import time:       344 |        344 |                                 base64
import time:       192 |        535 |                               email.base64mime
import time:       153 |        153 |                                   quopri
import time:       105 |        257 |                                 email.encoders
import time:       223 |        480 |                               email.charset
import time:       695 |       2216 |                             email.header
import time:       181 |        181 |                                 urllib
import time:      1539 |       1539 |                                 ipaddress
import time:      1116 |       2835 |                               urllib.parse
import time:        87 |         87 |                                     _locale
import time:       700 |        786 |                                   locale
import time:      1121 |       1906 |                                 calendar
import time:       311 |       2217 |                               email._parseaddr
import time:       503 |       5554 |                             email.utils
import time:       321 |       8090 |                           email._policybase
import time:       464 |       9038 |                         email.feedparser
import time:       349 |       9544 |                       email.parser
import time:       205 |        205 |                         email._encoded_words
import time:       118 |        118 |                         email.iterators
import time:       444 |        766 |                       email.message
import time:      1176 |       1176 |                         _ssl
import time:      2240 |       3415 |                       ssl
import time:       916 |      15334 |                     http.client
import time:       273 |        273 |                     shlex
import time:       192 |        192 |                         importlib._abc
import time:       137 |        328 |                       importlib.util
import time:      6200 |       6527 |                     botocore.vendored.six
import time:      2665 |       2665 |                         dateutil._version
import time:      1312 |       3976 |                       dateutil
import time:      4745 |       4745 |                         six
import time:        45 |         45 |                         six.moves
import time:      2056 |       2056 |                         dateutil.tz._common
import time:      1089 |       1089 |                         dateutil.tz._factories
import time:        29 |         29 |                           six.moves.winreg
import time:      1994 |       2022 |                         dateutil.tz.win
import time:      8267 |      18223 |                       dateutil.tz.tz
import time:       912 |      23110 |                     dateutil.tz
import time:        58 |         58 |                         _typing
import time:      2975 |       3033 |                       typing
import time:      5996 |       5996 |                       urllib3.exceptions
import time:      1576 |       1576 |                               urllib3.util.timeout
import time:      3763 |       5338 |                             urllib3.util.connection
import time:       925 |        925 |                               urllib3.util.util
import time:       100 |        100 |                               brotlicffi
import time:        87 |         87 |                               brotli
import time:        89 |         89 |                               zstandard
import time:      2076 |       3275 |                             urllib3.util.request
import time:       854 |        854 |                             urllib3.util.response
import time:      2573 |       2573 |                             urllib3.util.retry
import time:       252 |        252 |                               hmac
import time:      8511 |       8511 |                               urllib3.util.url
import time:      2046 |       2046 |                               urllib3.util.ssltransport
import time:      2199 |      13006 |                             urllib3.util.ssl_
import time:      1111 |       1111 |                             urllib3.util.wait
import time:      1007 |      27162 |                           urllib3.util
import time:        30 |      27192 |                         urllib3.util.connection
import time:      1851 |      29043 |                       urllib3._base_connection
import time:      3180 |       3180 |                       urllib3._collections
import time:       580 |        580 |                       urllib3._version
import time:        78 |         78 |                                 _winapi
import time:       102 |        102 |                                 winreg
import time:       434 |        612 |                               mimetypes
import time:      1646 |       2258 |                             urllib3.fields
import time:       982 |       3240 |                           urllib3.filepost
import time:       331 |        331 |                             zlib
import time:        97 |         97 |                             brotlicffi
import time:        85 |         85 |                             brotli
import time:        84 |         84 |                             zstandard
import time:      1295 |       1295 |                               urllib3.util.ssl_match_hostname
import time:      3902 |       5197 |                             urllib3.connection
import time:      5173 |      10965 |                           urllib3.response
import time:      1379 |      15583 |                         urllib3._request_methods
import time:       619 |        619 |                         urllib3.util.proxy
import time:      3896 |      20097 |                       urllib3.connectionpool
import time:      3176 |       3176 |                       urllib3.poolmanager
import time:        98 |         98 |                       urllib3_secure_extra
import time:      2025 |      67224 |                     urllib3
import time:       198 |        198 |                         xml
import time:       219 |        416 |                       xml.etree
import time:       368 |        368 |                         xml.etree.ElementPath
import time:       543 |        543 |                           pyexpat
import time:       333 |        875 |                         _elementtree
import time:       746 |       1988 |                       xml.etree.ElementTree
import time:       172 |       2575 |                     xml.etree.cElementTree
import time:        88 |         88 |                       awscrt
import time:        29 |        117 |                     awscrt.auth
import time:       169 |        169 |                       _compression
import time:       327 |        495 |                     gzip
import time:      3166 |     119546 |                   botocore.compat
import time:       824 |        824 |                         html.entities
import time:       382 |       1206 |                       html
import time:       406 |        406 |                       _markupbase
import time:      1612 |       3223 |                     html.parser
import time:      2229 |       5451 |                   botocore.docs.bcdoc.docstringparser
import time:      2839 |       2839 |                   botocore.docs.bcdoc.style
import time:      2818 |     131547 |                 botocore.docs.bcdoc.restdoc
import time:       135 |        135 |                           fnmatch
import time:        85 |         85 |                             _winapi
import time:        79 |         79 |                             nt
import time:        70 |         70 |                             nt
import time:        82 |         82 |                             nt
import time:        72 |         72 |                             nt
import time:        73 |         73 |                             nt
import time:        70 |         70 |                             nt
import time:       131 |        659 |                           ntpath
import time:       716 |       1509 |                         pathlib
import time:       286 |        286 |                                 _bz2
import time:       330 |        615 |                               bz2
import time:       378 |        378 |                                 _lzma
import time:       478 |        855 |                               lzma
import time:       512 |       1982 |                             shutil
import time:       315 |       2296 |                           tempfile
import time:       175 |        175 |                             urllib.response
import time:       208 |        382 |                           urllib.error
import time:      1552 |       4229 |                         urllib.request
import time:      1050 |       1050 |                               dateutil._common
import time:      2968 |       4018 |                             dateutil.relativedelta
import time:      7682 |      11699 |                           dateutil.parser._parser
import time:      2059 |       2059 |                           dateutil.parser.isoparser
import time:      1534 |      15291 |                         dateutil.parser
import time:      3007 |       3007 |                         botocore.awsrequest
import time:       920 |        920 |                             urllib3.contrib
import time:        74 |         74 |                               OpenSSL
import time:        25 |         99 |                             OpenSSL.SSL
import time:      4066 |       5083 |                           urllib3.contrib.pyopenssl
import time:       359 |        359 |                                   importlib.resources.abc
import time:       353 |        353 |                                   importlib.resources._adapters
import time:       477 |       1188 |                                 importlib.resources._common
import time:       223 |        223 |                                 importlib.resources._legacy
import time:       265 |       1675 |                               importlib.resources
import time:      2328 |       4003 |                             certifi.core
import time:      1107 |       5109 |                           certifi
import time:      2877 |      13068 |                         botocore.httpsession
import time:     15381 |      52482 |                       botocore.utils
import time:      1084 |      53566 |                     botocore.docs.shape
import time:      1522 |       1522 |                     botocore.docs.utils
import time:      1627 |      56713 |                   botocore.docs.example
import time:      1995 |       1995 |                     botocore.docs.params
import time:      1450 |       3445 |                   botocore.docs.method
import time:      1605 |       1605 |                   botocore.docs.sharedexample
import time:      2610 |      64372 |                 botocore.docs.client
import time:      1318 |       1318 |                 botocore.docs.paginator
import time:      1093 |       1093 |                 botocore.docs.waiter
import time:      3153 |     201481 |               botocore.docs.service
import time:      1636 |     203117 |             botocore.docs
import time:      1037 |     204154 |           botocore.docs.docstring
import time:      2504 |     238930 |         botocore.waiter
import time:      2959 |       2959 |             botocore.eventstream
import time:      4886 |       7844 |           botocore.parsers
import time:      2668 |       2668 |             botocore.validate
import time:      3865 |       6532 |           botocore.serialize
import time:        73 |         73 |                 _uuid
import time:       529 |        602 |               uuid
import time:       979 |        979 |               botocore.history
import time:      2781 |       2781 |               botocore.hooks
import time:      1370 |       1370 |                 botocore.response
import time:      2575 |       3944 |               botocore.httpchecksum
import time:      2545 |      10850 |             botocore.endpoint
import time:      1332 |      12182 |           botocore.config
import time:      5471 |       5471 |             botocore.auth
import time:       957 |        957 |             botocore.crt
import time:      3826 |       3826 |             botocore.endpoint_provider
import time:      3396 |      13649 |           botocore.regions
import time:      2541 |       2541 |           botocore.signers
import time:        37 |         37 |               botocore.customizations
import time:        26 |         63 |             botocore.customizations.useragent
import time:      2052 |       2115 |           botocore.useragent
import time:      2907 |      47767 |         botocore.args
import time:      1283 |       1283 |         botocore.compress
import time:       304 |        304 |             termios
import time:       200 |        503 |           getpass
import time:       484 |        484 |             signal
import time:       276 |        276 |             fcntl
import time:        89 |         89 |             msvcrt
import time:       169 |        169 |             _posixsubprocess
import time:       625 |       1641 |           subprocess
import time:      1175 |       1175 |             configparser
import time:      1404 |       2579 |           botocore.configloader
import time:      2315 |       2315 |           botocore.tokens
import time:      8087 |      15123 |         botocore.credentials
import time:      4083 |       4083 |           botocore.model
import time:      2047 |       6129 |         botocore.discovery
import time:      3074 |       3074 |         botocore.paginate
import time:      1002 |       1002 |         botocore.retries
import time:      1167 |       1167 |           botocore.retries.bucket
import time:       837 |        837 |             botocore.retries.quota
import time:       612 |        612 |               botocore.retries.base
import time:       882 |       1494 |             botocore.retries.special
import time:      2382 |       4712 |           botocore.retries.standard
import time:       873 |        873 |           botocore.retries.throttling
import time:      2707 |       9457 |         botocore.retries.adaptive
import time:      5685 |     328446 |       botocore.client
import time:      1741 |       1741 |         botocore.retryhandler
import time:       678 |        678 |         botocore.translate
import time:      5960 |       8378 |       botocore.handlers
import time:      2656 |       2656 |       botocore.monitoring
import time:      3620 |       3620 |       botocore.configprovider
import time:      1016 |       1016 |       botocore.errorfactory
import time:      1885 |       1885 |       botocore.loaders
import time:      5061 |     351634 |     botocore.session
import time:       939 |        939 |     boto3.utils
import time:       748 |        748 |       boto3.resources
import time:       738 |        738 |           boto3.docs.client
import time:       911 |        911 |               boto3.docs.base
import time:       878 |        878 |               boto3.docs.method
import time:      1310 |       1310 |               boto3.docs.utils
import time:      1322 |       4420 |             boto3.docs.action
import time:       812 |        812 |             boto3.docs.attr
import time:      1542 |       1542 |             boto3.docs.collection
import time:      1242 |       1242 |             boto3.docs.subresource
import time:      1202 |       1202 |             boto3.docs.waiter
import time:      2142 |      11359 |           boto3.docs.resource
import time:      4560 |      16656 |         boto3.docs.service
import time:      1104 |      17759 |       boto3.docs
import time:       967 |        967 |       boto3.docs.docstring
import time:      2357 |       2357 |         boto3.resources.model
import time:      1125 |       1125 |         boto3.resources.params
import time:      1329 |       1329 |         boto3.resources.response
import time:      1261 |       6071 |       boto3.resources.action
import time:      1101 |       1101 |       boto3.resources.base
import time:      1884 |       1884 |       boto3.resources.collection
import time:      4119 |      32645 |     boto3.resources.factory
import time:      2010 |     387435 |   boto3.session
import time:      3584 |     422564 | boto3
import time:      1792 |       1792 |       numpy._utils._convertions
import time:      1065 |       2856 |     numpy._utils
import time:      4987 |       7843 |   numpy._globals
import time:      1247 |       1247 |   numpy.exceptions
import time:       812 |        812 |   numpy.version
import time:        37 |         37 |     numpy._distributor_init_local
import time:      1031 |       1067 |   numpy._distributor_init
import time:      1391 |       1391 |             numpy._utils._inspect
import time:      4203 |       4203 |               numpy.core._exceptions
import time:      2220 |       2220 |               numpy.dtypes
import time:     47163 |      53585 |             numpy.core._multiarray_umath
import time:      2926 |      57902 |           numpy.core.overrides
import time:      7971 |      65872 |         numpy.core.multiarray
import time:      5783 |       5783 |         numpy.core.umath
import time:      2581 |       2581 |           numpy.core._string_helpers
import time:       213 |        213 |                 pickle5
import time:       577 |        577 |                   _compat_pickle
import time:       977 |        977 |                   _pickle
import time:      2721 |       4274 |                 pickle
import time:      4231 |       8717 |               numpy.compat.py3k
import time:      3481 |      12197 |             numpy.compat
import time:      5065 |       5065 |             numpy.core._dtype
import time:      6900 |      24161 |           numpy.core._type_aliases
import time:      8037 |      34778 |         numpy.core.numerictypes
import time:       469 |        469 |                     _contextvars
import time:       689 |       1157 |                   contextvars
import time:      3892 |       5048 |                 numpy.core._ufunc_config
import time:      7476 |      12524 |               numpy.core._methods
import time:     14225 |      26748 |             numpy.core.fromnumeric
import time:      7659 |      34407 |           numpy.core.shape_base
import time:     18204 |      18204 |           numpy.core.arrayprint
import time:      3078 |       3078 |           numpy.core._asarray
import time:     17565 |      73253 |         numpy.core.numeric
import time:     17894 |      17894 |         numpy.core.defchararray
import time:     10865 |      10865 |         numpy.core.records
import time:      8270 |       8270 |         numpy.core.memmap
import time:      8022 |       8022 |         numpy.core.function_base
import time:     10691 |      10691 |         numpy.core._machar
import time:     12212 |      12212 |         numpy.core.getlimits
import time:     18510 |      18510 |         numpy.core.einsumfunc
import time:      4183 |       4183 |           numpy.core._multiarray_tests
import time:     19182 |      23365 |         numpy.core._add_newdocs
import time:      2135 |       2135 |         numpy.core._add_newdocs_scalars
import time:      1353 |       1353 |         numpy.core._dtype_ctypes
import time:       512 |        512 |             _ctypes
import time:       290 |        290 |             ctypes._endian
import time:      2091 |       2892 |           ctypes
import time:      4432 |       7324 |         numpy.core._internal
import time:      1356 |       1356 |         numpy._pytesttester
import time:      8381 |     310056 |       numpy.core
import time:        34 |     310089 |     numpy.core._multiarray_umath
import time:      1585 |     311673 |   numpy.__config__
import time:      6496 |       6496 |     numpy.lib.mixins
import time:      1105 |       1105 |         numpy.lib.ufunclike
import time:      2173 |       3278 |       numpy.lib.type_check
import time:      3751 |       7028 |     numpy.lib.scimath
import time:      1968 |       1968 |                 numpy.lib.stride_tricks
import time:      2291 |       4259 |               numpy.lib.twodim_base
import time:      1709 |       1709 |               numpy.linalg._umath_linalg
import time:      3938 |       3938 |                 numpy._typing._nested_sequence
import time:       668 |        668 |                 numpy._typing._nbit
import time:      1909 |       1909 |                 numpy._typing._char_codes
import time:       901 |        901 |                 numpy._typing._scalars
import time:       579 |        579 |                 numpy._typing._shape
import time:      2284 |       2284 |                 numpy._typing._dtype_like
import time:      2896 |       2896 |                 numpy._typing._array_like
import time:      1954 |      15125 |               numpy._typing
import time:      8540 |      29631 |             numpy.linalg.linalg
import time:      1296 |      30926 |           numpy.linalg
import time:      4079 |      35005 |         numpy.matrixlib.defmatrix
import time:      1447 |      36451 |       numpy.matrixlib
import time:      3048 |       3048 |         numpy.lib.histograms
import time:     13808 |      16856 |       numpy.lib.function_base
import time:      3018 |      56324 |     numpy.lib.index_tricks
import time:      3627 |       3627 |     numpy.lib.nanfunctions
import time:      2984 |       2984 |     numpy.lib.shape_base
import time:      4077 |       4077 |     numpy.lib.polynomial
import time:      4251 |       4251 |     numpy.lib.utils
import time:      2735 |       2735 |     numpy.lib.arraysetops
import time:      2799 |       2799 |       numpy.lib.format
import time:      2171 |       2171 |       numpy.lib._datasource
import time:      3395 |       3395 |       numpy.lib._iotools
import time:      7182 |      15546 |     numpy.lib.npyio
import time:      1428 |       1428 |     numpy.lib.arrayterator
import time:      2928 |       2928 |     numpy.lib.arraypad
import time:      1181 |       1181 |     numpy.lib._version
import time:      2177 |     110776 |   numpy.lib
import time:       943 |        943 |       numpy.fft._pocketfft_internal
import time:      3747 |       4690 |     numpy.fft._pocketfft
import time:      1083 |       1083 |     numpy.fft.helper
import time:      1119 |       6891 |   numpy.fft
import time:      2703 |       2703 |       numpy.polynomial.polyutils
import time:      3927 |       3927 |       numpy.polynomial._polybase
import time:      8857 |      15487 |     numpy.polynomial.polynomial
import time:      3503 |       3503 |     numpy.polynomial.chebyshev
import time:      2790 |       2790 |     numpy.polynomial.legendre
import time:      2856 |       2856 |     numpy.polynomial.hermite
import time:      2929 |       2929 |     numpy.polynomial.hermite_e
import time:      2790 |       2790 |     numpy.polynomial.laguerre
import time:      2001 |      32353 |   numpy.polynomial
import time:        78 |         78 |             backports_abc
import time:      4047 |       4124 |           numpy.random._common
import time:       172 |        172 |           secrets
import time:      2772 |       7067 |         numpy.random.bit_generator
import time:      1662 |       1662 |         numpy.random._bounded_integers
import time:      1472 |       1472 |         numpy.random._mt19937
import time:      8317 |      18517 |       numpy.random.mtrand
import time:      1542 |       1542 |       numpy.random._philox
import time:      1664 |       1664 |       numpy.random._pcg64
import time:      1384 |       1384 |       numpy.random._sfc64
import time:      3873 |       3873 |       numpy.random._generator
import time:      4611 |      31589 |     numpy.random._pickle
import time:      1380 |      32968 |   numpy.random
import time:      2748 |       2748 |   numpy.ctypeslib
import time:     29009 |      29009 |     numpy.ma.core
import time:      6710 |       6710 |     numpy.ma.extras
import time:      1133 |      36852 |   numpy.ma
import time:     13048 |     558273 | numpy
import time:        89 |         89 |       chardet
import time:      4947 |       4947 |             charset_normalizer.constant
import time:      1019 |       1019 |               charset_normalizer.md__mypyc
import time:       292 |        292 |                 unicodedata
import time:       240 |        240 |                 _multibytecodec
import time:      2500 |       3031 |               charset_normalizer.utils
import time:      2105 |       6154 |             charset_normalizer.md
import time:      3039 |       3039 |             charset_normalizer.models
import time:      2431 |      16570 |           charset_normalizer.cd
import time:      4711 |      21280 |         charset_normalizer.api
import time:       992 |        992 |         charset_normalizer.legacy
import time:       717 |        717 |         charset_normalizer.version
import time:      1189 |      24176 |       charset_normalizer
import time:      2043 |       2043 |       http.cookiejar
import time:      1044 |       1044 |       http.cookies
import time:       964 |      28314 |     requests.compat
import time:      6070 |      34383 |   requests.exceptions
import time:       110 |        110 |   chardet
import time:        70 |         70 |     chardet
import time:      2833 |       2833 |       idna.package_data
import time:      3689 |       3689 |         idna.idnadata
import time:       905 |        905 |         idna.intranges
import time:      3078 |       7671 |       idna.core
import time:      1230 |      11732 |     idna
import time:      1293 |      13094 |   requests.packages
import time:        96 |         96 |         zipfile._path.glob
import time:       401 |        497 |       zipfile._path
import time:       139 |        139 |       zipfile.__main__
import time:       651 |       1285 |     zipfile
import time:       670 |        670 |     requests.certs
import time:       904 |        904 |     requests.__version__
import time:       991 |        991 |     requests._internal_utils
import time:      2560 |       2560 |     requests.cookies
import time:      1027 |       1027 |     requests.structures
import time:        91 |         91 |         importlib.resources._itertools
import time:       287 |        378 |       importlib.resources.readers
import time:       144 |        522 |     importlib.readers
import time:      4215 |      12171 |   requests.utils
import time:      1928 |       1928 |         requests.auth
import time:       256 |        256 |             stringprep
import time:       277 |        533 |           encodings.idna
import time:       750 |        750 |           requests.hooks
import time:      1594 |       1594 |           requests.status_codes
import time:      3883 |       6759 |         requests.models
import time:        80 |         80 |           socks
import time:      1465 |       1545 |         urllib3.contrib.socks
import time:      2259 |      12489 |       requests.adapters
import time:      2922 |      15411 |     requests.sessions
import time:       940 |      16351 |   requests.api
import time:      2190 |      78298 | requests

2. AWS Lambda 関数 (ZIP) x Layer x -X importtime オプション

AWS Lambda では拡張機能 (Lambda extensions) を使ってランタイムに任意のオプションを追加することができる.以下のドキュメントにはラッパースクリプトを書いて Python ランタイムに -X importtime オプションを追加する例が載っている👀

docs.aws.amazon.com

👾 template.yaml

AWS SAM の template.yaml を以下のように書いた.

Python ランタイムに -X importtime オプションを追加するラッパースクリプトを含んだ AWS Lambda Layer を作る.そして AWS Lambda 関数の環境変数 AWS_LAMBDA_EXEC_WRAPPER/opt/importtime_wrapper を設定する.

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31

Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: importtime-wrapper
      CodeUri: src/
      Handler: app.lambda_handler
      Runtime: python3.12
      Architectures:
        - x86_64
      Layers:
        - !Ref Layer
      Environment:
        Variables:
          AWS_LAMBDA_EXEC_WRAPPER: /opt/importtime_wrapper
  Layer:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: importtime-wrapper-layer
      ContentUri: layer/src/
      CompatibleRuntimes:
        - python3.12

👾 importtime_wrapper

importtime_wrapper はドキュメントの通りにしておく.そして,デプロイする前に忘れずに chmod +x layer/src/importtime_wrapper コマンドを実行してラッパースクリプトに実行権限を与えておく.

#!/bin/bash

# the path to the interpreter and all of the originally intended arguments
args=("$@")

# the extra options to pass to the interpreter
extra_args=("-X" "importtime")

# insert the extra options
args=("${args[@]:0:$#-1}" "${extra_args[@]}" "${args[@]: -1}")

# start the runtime with the extra options
exec "${args[@]}"

実行結果(割愛)

AWS Lambda 関数 importtime-wrapper をデプロイして実行すると同じく import 時間がログに出力されていた❗️

3. AWS Lambda 関数 (Image) x PYTHONPROFILEIMPORTTIME 環境変数

👾 template.yaml

AWS SAM の template.yaml を以下のように書いた.

Environment.VariablesPYTHONPROFILEIMPORTTIME: 1 を設定している😃

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31

Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: importtime-env-image
      PackageType: Image
      Architectures:
        - x86_64
      Environment:
        Variables:
          PYTHONPROFILEIMPORTTIME: 1
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./src
      DockerTag: python3.12-v1

👾 Dockerfile

FROM public.ecr.aws/lambda/python:3.12

COPY app.py requirements.txt ./

RUN python3.12 -m pip install -r requirements.txt -t .

CMD ["app.lambda_handler"]

実行結果(割愛)

AWS Lambda 関数 importtime-env-image をデプロイして実行すると同じく import 時間がログに出力されていた❗️

4. AWS Lambda 関数 (Image) x -X importtime オプション

以下のドキュメントを参考に python:3.12 など一般的なコンテナイメージ (non-AWS base image) に AWS Lambda Runtime Interface Client (RIC) をインストールして実行するパターンを試す.Dockerfile のサンプルもドキュメントを参考にした.

docs.aws.amazon.com

👾 template.yaml

AWS SAM の template.yaml を以下のように書いた.

AWSTemplateFormatVersion: 2010-09-09
Transform: AWS::Serverless-2016-10-31

Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: importtime-option-image
      PackageType: Image
      Architectures:
        - x86_64
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./src
      DockerTag: python3.12-v1

👾 Dockerfile

ポイントは ENTRYPOINT-X importtime オプションを追加しているところ📝

ARG FUNCTION_DIR="/function"

FROM python:3.12 as build-image

ARG FUNCTION_DIR
RUN mkdir -p ${FUNCTION_DIR}
COPY . ${FUNCTION_DIR}

RUN pip install --target ${FUNCTION_DIR} -r ${FUNCTION_DIR}/requirements.txt

FROM python:3.12-slim

ARG FUNCTION_DIR
WORKDIR ${FUNCTION_DIR}
COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}

ENTRYPOINT [ "/usr/local/bin/python", "-X", "importtime", "-m", "awslambdaric" ]
CMD ["app.lambda_handler"]

👾 requirements.txt

requirements.txtawslambdaric を追加した.

awslambdaric
boto3
numpy
requests

実行結果(割愛)

AWS Lambda 関数 importtime-option-image をデプロイして実行すると同じく import 時間がログに出力されていた❗️

まとめ

AWS Lambda 関数 (Python) で import の最適化をするときには Python の -X importtime オプション(もしくは環境変数 PYTHONPROFILEIMPORTTIME)が便利〜👏 という紹介記事でした \( 'ω')/

参考記事

以下の記事でも AWS Lambda 関数 (Python) に PYTHONPROFILEIMPORTTIME 環境変数を設定して INIT を最適化する方法が紹介されているので合わせて読んでみると良いかも📝

aws.amazon.com

AWS CDK で Amazon CloudWatch Alarm の AWS Lambda アクションを設定する

2023年12月のリリースで Amazon CloudWatch Alarm から直接 AWS Lambda 関数を呼び出して,何かしらのアクション(復旧処理など)を実行できるようになった❗️今までは Amazon SNS と組み合わせて実行する必要があって,今までよりもシンプルに統合できるようになった.もちろん Amazon SNS でファンアウトすることによる拡張性も重要なので,個人的には AWS Lambda 関数を呼び出せれば十分というシンプルな場面で使えそうだなーと思っていたりする \( 'ω')/

aws.amazon.com

AWS CDK で試す

AWS CDK では v2.119.0 から Amazon CloudWatch Alarm の Lambda アクションを設定できるようになっていた❗️試してみたサンプルコード (TypeScript) を載せておく.

github.com

docs.aws.amazon.com

👾 sandbox-cdk-cloudwatch-lambda-stack.ts

Amazon SQS キューの ApproximateNumberOfMessagesVisible メトリクスを監視して,メッセージ数が増えたら Amazon CloudWatch Alarm から直接 AWS Lambda 関数を呼び出すようにした(Lambda 関数部分は割愛).今回のポイントは最後の addAlarmAction()aws_cloudwatch_actions.LambdaAction を指定しているところ❗️

import {
    Duration,
    Stack,
    StackProps,
    aws_cloudwatch,
    aws_cloudwatch_actions,
    aws_iam,
    aws_lambda,
    aws_sqs,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import path = require('path');

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

        const queue = new aws_sqs.Queue(this, 'SandboxCdkQueue', {
            queueName: 'sandbox-cdk-cloudwatch-lambda-queue',
            visibilityTimeout: Duration.seconds(30),
            receiveMessageWaitTime: Duration.seconds(20),
        });

        const lambdaBasicExecutionRole = new aws_iam.Role(this, 'SandboxCdkLambdaBasicExecutionRole', {
            assumedBy: new aws_iam.ServicePrincipal('lambda.amazonaws.com'),
            managedPolicies: [
                aws_iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
            ]
        });

        const lambda = new aws_lambda.Function(this, 'SandboxCdkLambdaFunction', {
            functionName: 'sandbox-cdk-cloudwatch-lambda-function',
            runtime: aws_lambda.Runtime.PYTHON_3_12,
            code: aws_lambda.Code.fromAsset(path.join(__dirname, '../functions/hello')),
            handler: 'app.lambda_handler',
            role: lambdaBasicExecutionRole,
        });

        const alarm = new aws_cloudwatch.Alarm(this, 'SandboxCdkCloudWatchAlarm', {
            alarmName: 'sandbox-cdk-cloudwatch-lambda-queue-alarm',
            metric: new aws_cloudwatch.Metric({
                namespace: 'AWS/SQS',
                metricName: 'ApproximateNumberOfMessagesVisible',
                dimensionsMap: {
                    QueueName: queue.queueName
                },
                period: Duration.seconds(60),
                statistic: aws_cloudwatch.Stats.SUM,
            }),
            threshold: 5,
            evaluationPeriods: 1,
        });

        alarm.addAlarmAction(new aws_cloudwatch_actions.LambdaAction(lambda));
    }
}

結果

Amazon SQS キューにメッセージを5件以上送信すると(今回は10件)アラーム状態になって AWS Lambda 関数が呼び出された👌

アラーム状態と AWS Lambda アクション設定

注意点

現時点(最新は v2.121.1)では同じ AWS Lambda 関数を複数の Amazon CloudWatch Alarm に紐付けるとエラーになってしまうという問題がある.プルリクエストも出ていて,近々直りそうな気もするけど注意しておくと良いかなーと👀

Error: There is already a Construct with name 'AlarmPermission' in Function [SandboxCdkLambdaFunction]

AWS CDK で AWS Lambda 関数の高度なログ制御機能(フォーマット・ログレベル・ログ集約)を設定する

2023年11月にリリースされた AWS Lambda 関数の「高度なログ制御機能 (advanced logging controls)」によって,大きく3種類の追加設定ができるようになった💡どれも AWS Lambda 関数を多く運用しているチームになどに嬉しいアップデートではあるけど,個人的には特に「任意の Amazon CloudWatch Logs Group」にログを集約できるのは便利だと思う❗️

  1. AWS Lambda 関数のログフォーマットを JSON に変更できる
  2. AWS Lambda 関数のログレベルをコードを変更せずに設定できる
  3. 任意の Amazon CloudWatch Logs Group にログを集約できる

むしろ今までの経験から AWS Lambda 関数の Amazon CloudWatch Logs Group は /aws/lambda/xxxxx である(そう仕様で決まっている✔️)という認識がまだ頭のどこかに残ってて,ログ集約の設定をした AWS Lambda 関数のモニタリング設定を見ると見慣れない感じがあるのは僕だけ?笑

aws.amazon.com

aws.amazon.com

高度なログ制御機能の詳細(ログライブラリの話など)は以下のドキュメントにまとまっている📝 現状だとまだ日本語版は内容が古そう.

docs.aws.amazon.com

AWS CDK で試す

AWS CDK では v2.110.0 から AWS Lambda 関数の高度なログ制御機能を設定できるようになっていた❗️試してみたサンプルコード (TypeScript) を載せておく.

github.com

docs.aws.amazon.com

👾 sandbox-cdk-serverless-stack.ts

サンプルコードでは以下のように高度なログ制御機能を設定した💡

  • logFormat でログフォーマットを JSON に
  • systemLogLevel でシステムログレベルを WARN に
  • applicationLogLevel でアプリケーションログレベルを INFO に
  • logGroup で Amazon CloudWatch Logs Group sandbox-cdk-aggregated-lambda-log-group にログを集約するように
import {
  RemovalPolicy,
  Stack,
  StackProps,
  aws_iam,
  aws_lambda,
  aws_logs,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';
import path = require('path');

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

    const lambdaBasicExecutionRole = new aws_iam.Role(this, 'SandboxCdkLambdaBasicExecutionRole', {
      roleName: 'lambda-basic-execution-role',
      assumedBy: new aws_iam.ServicePrincipal('lambda.amazonaws.com'),
      managedPolicies: [
        aws_iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')
      ]
    });

    const aggregatedLambdaLogGroup = new aws_logs.LogGroup(this, 'SandboxCdkAggregatedLambdaLogGroup', {
      logGroupName: 'sandbox-cdk-aggregated-lambda-log-group',
      retention: aws_logs.RetentionDays.ONE_WEEK,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    new aws_lambda.Function(this, 'SandboxCdkLambdaFunctionHello', {
      functionName: 'sandbox-cdk-hello-function',
      runtime: aws_lambda.Runtime.PYTHON_3_12,
      code: aws_lambda.Code.fromAsset(path.join(__dirname, '../functions/hello')),
      handler: 'app.lambda_handler',
      role: lambdaBasicExecutionRole,
      logFormat: aws_lambda.LogFormat.JSON,
      systemLogLevel: aws_lambda.SystemLogLevel.WARN,
      applicationLogLevel: aws_lambda.ApplicationLogLevel.INFO,
      logGroup: aggregatedLambdaLogGroup,
    });

    new aws_lambda.Function(this, 'SandboxCdkAwsLambdaFunctionBye', {
      functionName: 'sandbox-cdk-bye-function',
      runtime: aws_lambda.Runtime.PYTHON_3_12,
      code: aws_lambda.Code.fromAsset(path.join(__dirname, '../functions/bye')),
      handler: 'app.lambda_handler',
      role: lambdaBasicExecutionRole,
      logFormat: aws_lambda.LogFormat.JSON,
      systemLogLevel: aws_lambda.SystemLogLevel.WARN,
      applicationLogLevel: aws_lambda.ApplicationLogLevel.INFO,
      logGroup: aggregatedLambdaLogGroup,
    });
  }
}

結果

期待通り「ロギング設定」を設定できた❗️

ロギング設定を設定できた

関連記事

kakakakakku.hatenablog.com

正式リリースになった AWS SAM CLI の Terraform サポート機能を試す

2023年9月5日に AWS SAM CLI の Terraform サポート機能が GA (正式リリース)になった👏

Amazon API Gateway や AWS Lambda 関数などサーバーレス関連のコンポーネントは Terraform で統一的に管理しつつも,AWS SAM CLI の開発支援機能(sam local invoke コマンドや sam local start-api コマンドでローカルデバッグ)は使いたい❗️という場面はあって非常に便利な組み合わせだと思う.

aws.amazon.com

実際にどういう開発体験なのかを確認するために AWS ブログに載っていたサンプルを試してみる \( 'ω')/

aws.amazon.com

検証環境

今回は macOS 上で SAM CLI 1.97.0(最新)と Terraform 1.5.7(最新)を使う.

$ sam --version
SAM CLI, version 1.97.0

$ terraform version
Terraform v1.5.7
on darwin_arm64

GitHub リポジトリ確認

サンプルコードは GitHub aws-samples/aws-sam-terraform-examples リポジトリの ga ディレクトリにある❗️

github.com

ga ディレクトリには3種類のサンプルがあって,今回は Amazon API Gateway の REST API 前提の api_gateway_v1 を使う📝

$ tree ga -L 1
ga
├── README.md
├── api_gateway_v1
├── api_gateway_v2
└── api_gateway_v2_tf_cloud

4 directories, 1 file

さらに ga/api_gateway_v1 ディレクトリ配下のファイルをまとめた.tf-resources ディレクトリに main.tf など Terraform コードがあって,src ディレクトリに AWS Lambda 関数コード(今回は Python 実装)があって,events ディレクトリに AWS Lambda 関数 auth の動作確認に使うイベント情報がある👌

$ tree ga/api_gateway_v1
ga/api_gateway_v1
├── events
│   └── auth.json
├── src
│   ├── auth
│   │   ├── app.py
│   │   └── requirements.txt
│   └── responder
│       ├── app.py
│       └── requirements.txt
└── tf-resources
    ├── api.tf
    ├── functions.tf
    ├── main.tf
    ├── samconfig.yaml
    └── variables.tf

6 directories, 10 files

事前準備

サンプルコードを確認したところ,Python 3.9 の AWS Lambda 関数になっていた.せっかくなら最新の Python 3.11 で動作確認をしておきたくて ga/api_gateway_v1/tf-resources/functions.tf を修正した.

--- a/ga/api_gateway_v1/tf-resources/functions.tf
+++ b/ga/api_gateway_v1/tf-resources/functions.tf
@@ -5,7 +5,7 @@ module "lambda_function_responder" {
   source_path   = "../src/responder/"
   function_name = "responder"
   handler       = "app.open_handler"
-  runtime       = "python3.9"
+  runtime       = "python3.11"
   create_sam_metadata = true
   publish       = true
   allowed_triggers = {
@@ -23,6 +23,6 @@ module "lambda_function_auth" {
   source_path   = "../src/auth/"
   function_name = "authorizer"
   handler       = "app.handler"
-  runtime       = "python3.9"
+  runtime       = "python3.11"
   create_sam_metadata = true
 }

しかし AWS Provider 側で Python 3.11 がサポートされてなく,sam build コマンドの実行時に以下のエラーが出てしまう🔥

Error: expected runtime to be one of [nodejs nodejs4.3 nodejs6.10 nodejs8.10 nodejs10.x nodejs12.x nodejs14.x nodejs16.x java8 java8.al2 java11 python2.7 python3.6 python3.7 python3.8 python3.9 dotnetcore1.0 dotnetcore2.0 dotnetcore2.1 dotnetcore3.1 dotnet6 nodejs4.3-edge go1.x ruby2.5 ruby2.7 provided provided.al2 nodejs18.x python3.10 java17], got python3.11

そこで AWS Provider の最新を使えるようにga/api_gateway_v1/tf-resources/main.tf を修正して terraform init -upgrade コマンドを実行しておく.

--- a/ga/api_gateway_v1/tf-resources/main.tf
+++ b/ga/api_gateway_v1/tf-resources/main.tf
@@ -2,7 +2,7 @@ terraform {
   required_providers {
     aws = {
       source  = "hashicorp/aws"
-      version = "~> 4.16"
+      version = "~> 5.17"
     }
   }

準備完了👏

さっそく試す: sam buildsam local invoke

sam build コマンド

Terraform コードや設定など細かいことは後ほど確認するとして,まずは試してみる❗️

ga/api_gateway_v1/tf-resources ディレクトリに移動して,Terraform 変数にリージョンを指定して(今回は東京リージョンを使う),あとは使い慣れた sam build コマンドを実行する.しかし今回は --hook-name オプションで terraform を指定して実行する点に注意💡

実行すると Terraform の local-exec Provisioner の実行ログが大量に出てきて,不安になる(Ctrl+C を押したくなる)けどそのまま待ってると Build Succeeded という表示が出て完了する.

$ cd ga/api_gateway_v1/tf-resources

$ export TF_VAR_aws_region=ap-northeast-1

$ sam build --hook-name terraform --terraform-project-root-path ../
Running Prepare Hook to prepare the current application
Executing prepare hook of hook "terraform"

(中略)

module.lambda_function_auth.null_resource.archive[0] (local-exec):
module.lambda_function_responder.null_resource.archive[0] (local-exec):

(中略)

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

sam local invoke コマンド

そして使い慣れた sam local invoke コマンドに --hook-name terraform オプションを指定して実行すると,public.ecr.aws/lambda/python:3.11-rapid-x86_64 イメージを使って AWS Lambda 関数を実行できる.今回は responder を実行したら Hello TF World って返ってきた \( 'ω')/ うおお〜

$ sam local invoke --hook-name terraform 'module.lambda_function_responder.aws_lambda_function.this[0]'
Skipped prepare hook. Current application is already prepared.
Invoking app.open_handler (python3.11)
Local image was not found.
Removing rapid images for repo public.ecr.aws/sam/emulation-python3.11
Building image.............................................................................................................................................................................................................................................................................................................................................................................................................................
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

(中略)

START RequestId: 71b6ab8c-53f9-41ac-8fb0-62bf4ba929b4 Version: $LATEST
END RequestId: 71b6ab8c-53f9-41ac-8fb0-62bf4ba929b4
REPORT RequestId: 71b6ab8c-53f9-41ac-8fb0-62bf4ba929b4  Init Duration: 0.74 ms    Duration: 1551.46 ms  Billed Duration: 1552 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"Hello TF World\", \"location\": \"xxx.xxx.xxx.xxx\"}"}%

せっかくだから ga/api_gateway_v1/src/responder/app.py を修正して,もう一度 sam build コマンドと sam local invoke コマンドを実行すると,今度は Hello kakakakakku World って返ってきた \( 'ω')/ うおお〜

$ sam build --hook-name terraform --terraform-project-root-path ../

$ sam local invoke --hook-name terraform 'module.lambda_function_responder.aws_lambda_function.this[0]'
Skipped prepare hook. Current application is already prepared.
Invoking app.open_handler (python3.11)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

(中略)

START RequestId: f8ddd88f-bfd0-45c5-8643-6db922e6d798 Version: $LATEST
END RequestId: f8ddd88f-bfd0-45c5-8643-6db922e6d798
REPORT RequestId: f8ddd88f-bfd0-45c5-8643-6db922e6d798  Init Duration: 0.57 ms    Duration: 1201.38 ms  Billed Duration: 1202 ms   Memory Size: 128 MB    Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"Hello kakakakakku World\", \"location\": \"xxx.xxx.xxx.xxx\"}"}%

そして同じように auth も実行できた \( 'ω')/ うおお〜

$ sam local invoke --hook-name terraform 'module.lambda_function_auth.aws_lambda_function.this[0]' -e ../events/auth.json
Skipped prepare hook. Current application is already prepared.
Invoking app.handler (python3.11)
Local image is up-to-date
Using local image: public.ecr.aws/lambda/python:3.11-rapid-x86_64.

(中略)

START RequestId: c6503ad1-200d-453a-96d9-94c99d0e470a Version: $LATEST
END RequestId: c6503ad1-200d-453a-96d9-94c99d0e470a
REPORT RequestId: c6503ad1-200d-453a-96d9-94c99d0e470a  Init Duration: 0.52 ms    Duration: 269.49 ms   Billed Duration: 270 ms    Memory Size: 128 MB    Max Memory Used: 128 MB
{"principalId": "user|a1b2c3d4", "policyDocument": {"Version": "2012-10-17", "Statement": [{"Action": "execute-api:Invoke", "Effect": "Allow", "Resource": ["arn:aws:execute-api:us-east-1:123456789012:1234567890/prod/*/*"]}]}, "context": {"number": 1, "bool": true, "passed": "from authorizer"}}%

また sam local invoke コマンドの引数に実行する AWS Lambda 関数の Terraform リソース ID を指定する必要があるけど,指定なしで実行すると候補を表示してくれるようになってて親切設計だった👏

$ sam local invoke --hook-name terraform

Skipped prepare hook. Current application is already prepared.
Error: You must provide a function logical ID when there are more than one functions in your template. Possible options in your template: ['module.lambda_function_auth.aws_lambda_function.this[0]', 'module.lambda_function_responder.aws_lambda_function.this[0]']

ちなみに Terraform と AWS SAM CLI の連携に関しては以下のドキュメントに詳しく載っている📝 現時点だと最新情報は日本語に翻訳されてなく,まだ GA 前のドキュメントになっているため,英語を確認する必要がある.

docs.aws.amazon.com

samconfig ファイル

AWS SAM CLI でよく使う samconfig にも対応していて,例えば以下のように samconfig.toml を設定できる.

version = 0.1
[default]
[default.global]
[default.global.parameters]
hook_name = "terraform"
skip_prepare_infra = true
[default.build]
[default.build.parameters]
terraform_project_root_path = "../"

すると sam build コマンドと sam local invoke コマンドのオプションを減らせてシンプルに実行できるようになる👏

$ sam build
$ sam local invoke 'module.lambda_function_responder.aws_lambda_function.this[0]'

ちなみに2023年7月頃から AWS SAM CLI で YAML 形式の samconfig もサポートされててサンプルには samconfig.yaml が含まれた.

version: 0.1
default:
  global:
    parameters:
      hook_name: terraform
      skip_prepare_infra: true
  build:
    parameters:
      terraform_project_root_path: ../

さっそく試す: sam local start-api

今度は sam local start-api コマンドを実行して API の動作確認をする❗️

$ sam local start-api --hook-name terraform
Skipped prepare hook. Current application is already prepared.

AWS SAM CLI does not guarantee 100% fidelity between authorizers locally
and authorizers deployed on AWS. Any application critical behavior should
be validated thoroughly before deploying to production.

Testing application behaviour against authorizers deployed on AWS can be done using the sam sync command.

Mounting responder at http://127.0.0.1:3000/open [GET]
Mounting responder at http://127.0.0.1:3000/secure [GET]

(中略)

2023-09-19 22:00:00 Press CTRL+C to quit

すぐに localhost:3000/open API と /secure API を呼び出せた \( 'ω')/ うおお〜

ちなみに /secure API は Amazon API Gateway の Lambda オーソライザーを使ったアクセス制御になっているため,HTTP ヘッダーに myheader: 123456789 を設定した場合にレスポンスが返るように実装されている.

$ curl http://localhost:3000/open
{"message": "Hello kakakakakku World", "location": "xxx.xxx.xxx.xxx"}

$ curl http://localhost:3000/secure
{"message":"Unauthorized"}

$ curl http://localhost:3000/secure --header 'myheader: 123456789'
{"message": "Hello kakakakakku World", "location": "xxx.xxx.xxx.xxx"}

$ curl http://localhost:3000/secure --header 'myheader: IamInvalid'
{"message":"Unauthorized"}

Terraform コード

うまく実行できたので,次は Terraform コードを確認する❗️

module "lambda_function_responder" {
  source        = "terraform-aws-modules/lambda/aws"
  version       = "~> 6.0"
  timeout       = 300
  source_path   = "../src/responder/"
  function_name = "responder"
  handler       = "app.open_handler"
  runtime       = "python3.11"
  create_sam_metadata = true
  publish       = true
  allowed_triggers = {
    APIGatewayAny = {
      service    = "apigateway"
      source_arn = "${aws_api_gateway_rest_api.api.execution_arn}/*/*"
    }
  }
}

module "lambda_function_auth" {
  source        = "terraform-aws-modules/lambda/aws"
  version       = "~> 6.0"
  timeout       = 300
  source_path   = "../src/auth/"
  function_name = "authorizer"
  handler       = "app.handler"
  runtime       = "python3.11"
  create_sam_metadata = true
}

まず functions.tf を確認すると,AWS Provider の aws_lambda_function ではなく AWS Lambda Terraform module を使っていた.AWS ブログにも書いてある通り,あくまでこれは Terraform コードをシンプルに書くために使われていて,もちろん AWS SAM CLI の Terraform サポートは AWS Provider でも使える👌

github.com

そして AWS Lambda Terraform module を使うことでシンプルに書ける例として create_sam_metadata = true があって,これは Terraform の null_resource を使って sam metadata リソースを自動的に作ってくれる.sam metadata は AWS Lambda 関数のアーティファクトがどこにあるかを伝えるために必要で,さらに AWS Lambda 関数の ZIP ファイルを作るビルド処理にも依存している.sam metadata に関しては以下のドキュメントに載っている❗️

docs.aws.amazon.com

また AWS Lambda Terraform module の main.tf にある null_resource.sam_metadata_aws_lambda_functionpackage.tf にある null_resource.archive を読むと Terraform の local-exec Provisioner を使ってビルドをしてる流れ(sam build コマンドを実行するとログが大量に出てくる部分)など理解できるからおすすめ〜👀

以下のドキュメントには AWS Provider の aws_lambda_function を使ったサンプルも載っていた💡

docs.aws.amazon.com

まとめ

今月 GA になった AWS SAM CLI の Terraform サポート機能をさっそく試してみたけど良かった❗️

さっそく今の現場で実戦投入してみようと思う \( 'ω')/

Lambda の運用面でのベストプラクティスを学べる「AWS Lambda Operator Guide」を読んだ

AWS の公式ドキュメント「AWS Lambda Operator Guide」を読んだ❗️AWS Lambda を軸にサーバーレスアプリケーションを構築するときに意識しておくべき "運用面のポイント・ベストプラクティス" がまとまっていて,とても良いドキュメントだった👏 内容的には AWS Well-Architected Framework: Serverless Applications Lens と重複するところもあるけど,サーバーレスアプリケーションを開発・運用しているなら1度は読んでおくと良いのではないでしょうか❗️

\( 'ω')/ 多くの人に読みやすくなるように日本語翻訳もあるとイイなぁ〜

docs.aws.amazon.com

構成

ドキュメントとしては全6章で構成されている.どれも重要で,理解を深めるために読むのはもちろん,開発中もしくは運用中のアプリケーションに対して「これは検討できてるかな?こっちはどう?」という感じで自問自答しながらセルフレビューに活用できたりもする💡

  • 1. Event-driven architectures(イベント駆動型アーキテクチャ)
  • 2. Application design(アプリケーション設計)
  • 3. Security(セキュリティ)
  • 4. Debugging(デバッグ)
  • 5. Monitoring and observability(モニタリングとオブザーバビリティ)
  • 6. Performance optimization(パフォーマンス最適化)

⚡️1. Event-driven architectures(イベント駆動型アーキテクチャ)

トピック一覧

  • How Lambda fits into the event-driven paradigm(Lambda がイベント駆動型パラダイムにどのように適合するか)
  • The benefits of event-driven architectures(イベント駆動型アーキテクチャのメリット)
  • Trade-offs of event-driven architectures(イベント駆動型アーキテクチャのトレードオフ)
  • Design principles(設計原則)
  • Anti-patterns in Lambda-based applications(Lambda を使ったアプリケーションのアンチパターン)
  • Frequently asked questions(よくある質問)

メモ

AWS Lambda の活用例としてよく出てくる「イベント駆動型」に関するトピックを学べる.前半ではイベント駆動型のメリットやポーリング型との違いなどがまとまっている.後半ではトレードオフ・設計原則・アンチパターンまでまとまっていて,イベント駆動型アーキテクチャを実践するときに理解しておくべきポイントが凝縮されている.

Design principlesDeveloping for retries and failures では「冪等性」の実現例として,Amazon DynamoDB に TTL (Time To Live) 付きの項目を登録して,トランザクションが以前に処理されたかどうかを判断するテクニックが載っていた💡これはとても実践的で,実は最近仕事で似たような実装を検討していて,関連情報として使えるな〜なんて思ったりもした.

他には Anti-patterns in Lambda-based applicationsThe Lambda monolith(モノリスラムダ)という印象的な名前のアンチパターンも紹介されていたりもする❗️

⚡️2. Application design(アプリケーション設計)

トピック一覧

  • Understanding quotas(クォータを理解する)
  • Scaling and concurrency in Lambda(Lambda でのスケーリングと同時実行)
  • Choosing and managing runtimes in Lambda functions(Lambda 関数でのランタイムの選択と管理)
  • Networking and VPC configurations(ネットワーク構成と VPC 構成)
  • Comparing Lambda invocation modes(Lambda 呼び出しモードの比較)
  • Controlling traffic flow for server-based resources(サーバーを使ったリソースのトラフィックフローのコントロール)
  • Frequently asked questions(よくある質問)

メモ

サーバーレスアプリケーションをスケールしようとするときに重要になる AWS Lambda や Amazon API Gateway 関連のクォータ・設定項目がまとまっているのは良かった❗️同時実行数・バースト同時実行数・予約された同時実行数など.以下のドキュメントにも詳しく載っている.

docs.aws.amazon.com

Choosing and managing runtimes in Lambda functionsRuntimes and performance では AWS Lambda 関数のランタイムをどう選ぶかというトピックも載っていた.例えば Python / Node.js は初期化が高速で,Go は起動と実行どちらもパフォーマンスが良いなど.さらに Managing AWS SDKs in Lambda functions では,Python など一部のランタイムには AWS SDK(例えば boto3)がバンドルされているけど,Lambda レイヤーを使って AWS SDK のバージョンを固定するべきというベストプラクティスも載っていた❗️

docs.aws.amazon.com

AWS SDK のバージョンを固定するという話は The Twelve-Factor App の "依存関係" で宣言されている システム全体にインストールされるパッケージが暗黙的に存在することに決して依存しない を意識することにも繋がって非常に重要なプラクティスと言える👌

12factor.net

⚡️3. Security(セキュリティ)

トピック一覧

  • Understanding the Lambda execution environment(Lambda 実行環境の理解)
  • Applying the principles of least privilege(最小権限の法則を適用)
  • Securing workloads with public endpoints(パブリックエンドポイントでのワークロードの保護)
  • Encrypting data in Lambda-based applications(Lambda を使ったアプリケーションでのデータの暗号化)
  • Governance controls with AWS CloudTrail(AWS CloudTrail によるガバナンスコントロール)
  • Frequently asked questions(よくある質問)

メモ

Understanding the Lambda execution environment に載っている Lambda 実行環境の構成図は AWS Lambda 関数がどうやって立ち上がるのかを理解するのに良いと思う💡

また Avoiding granting wildcard permissions in IAM policies では AWS Lambda 関数に設定する IAM Role のポリシーでワイルドカードを回避して最小権限にするべきという話も載っている.理解はしつつもワイルドカードのまま設定されている AWS Lambda 関数はよく見るし,改めて意識しないとな〜と思えた.

⚡️4. Debugging(デバッグ)

  • Standardizing a debugging approach(デバッグアプローチの標準化)
  • Troubleshooting payloads(ペイロードのトラブルシューティング)
  • Troubleshooting integration errors(統合エラーのトラブルシューティング)
  • Troubleshooting Lambda configurations(Lambda 構成のトラブルシューティング)
  • Troubleshooting queue processing by Lambda functions(Lambda 関数によるキュー処理のトラブルシューティング)
  • Best practices for your debugging environment(デバッグ環境のベストプラクティス)

メモ

サーバーレスアプリケーションでは複数のコンポーネントを組み合わせることになるため,デバッグするポイントも増える.多種多様な観点で,どのようなトラブルが起きる可能性があるのか?という視野を広げられる内容になっている💡

例えば Troubleshooting payloads では,予期しないペイロードを受け取る可能性もあるので例外処理をしようというプラクティスが載っていて,すぐに使えそうだった.また Troubleshooting queue processing by Lambda functions では AWS Lambda と Amazon SQS を組み合わせる場合の注意点も詳しく載っているのも参考になる❗️

⚡️5. Monitoring and observability(モニタリングとオブザーバビリティ)

トピック一覧

  • Monitoring concepts in Lambda-based applications(Lambda を使ったアプリケーションのモニタリング概念)
  • Logging and metrics with Amazon CloudWatch(Amazon CloudWatch によるログとメトリクス)
  • Searching across logs with CloudWatch Logs Insights(CloudWatch Logs Insights を使ったログ全体の検索)
  • Tracing requests with AWS X-Ray(AWS X-Ray を使ったリクエストのトレース)
  • Troubleshooting walkthrough: isolating and resolving issues(トラブルシューティングウォークスルー: 問題の切り分けと解決)
  • A general approach to debugging Lambda performance issues and errors(Lambda のパフォーマンス問題とエラーをデバッグするための一般的なアプローチ)
  • Monitoring Lambda code storage(Lambda コールドストレージのモニタリング)

メモ

オブザーバビリティの定義としてよくある「メトリクス・ログ・トレース」それぞれをどのように実現するか?というトピックがまとまっている💡Logging and metrics with Amazon CloudWatchImportant metrics for CloudWatch では AWS Lambda をモニタリングするときに特に重要なメトリクス一覧が載っていて,Searching across logs with CloudWatch Logs InsightsUseful Insights queries では AWS Lambda 関数のログ検索でよく使う CloudWatch Logs Insights のクエリテンプレートも載っていて参考になる❗️

他にも Troubleshooting walkthrough: isolating and resolving issues は実際のアプリケーションでパフォーマンス問題を解決するというトラブルシューティングウォークスルーになっていて良かった💡

⚡️6. Performance optimization(パフォーマンス最適化)

トピック一覧

  • Lambda execution environments(Lambda 実行環境)
  • Memory and computing power(メモリと計算能力)
  • Optimizing static initialization(初期化の最適化)
  • Architecture and Best Practices(アーキテクチャとベストプラクティス)

メモ

Lambda 入門者が理解しておくと良い "コールドスタート" と "ウォームスタート" の違いが紹介されている.またドキュメントでは functions warmers という用語で紹介されていたけど,Amazon EventBridge を使って定期的に AWS Lambda 関数を呼び出し続けるという,前からよく知られたテクニックは,コールドスタートを削減することを保証するわけではないけど低優先度のワークロードであれば適しているかもしれないと紹介されているのも良かった💡

他には Profiling functions with AWS Lambda Power Tuning ではAWS Lambda 関数に割り当てる最適なメモリサイズを模索するために AWS Lambda Power TuningAWS Compute Optimizer を使うという話なども載っている.

github.com

まとめ

AWS の公式ドキュメント「AWS Lambda Operator Guide」を読んだ❗️サーバーレス(特に AWS Lambda)に対する理解度をグッと高められる良いドキュメントだった👏

サーバーレスアプリケーションを開発・運用しているなら1度は読んでおくと良いのではないでしょうか❗️

docs.aws.amazon.com